diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..936cc16 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PORT=8000 \ + MCP_TRANSPORT=streamable-http \ + MCP_HTTP_PATH=/mcp \ + CHAT_STORE_BACKEND=r2 + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY . /app + +EXPOSE 8000 + +CMD ["python", "claude_tool_mcp/server.py"] diff --git a/README.md b/README.md index f0e4a5e..04ea1dd 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,183 @@ -# DeepRecurse +# DeepRecurse — Cloudflare MCP + Session Upload -Local prototype for a shared-history chat interface where RLM execution lives in an MCP server tool. +Shared-history chat prototype where RLM execution lives in an MCP tool, deployed via Cloudflare Workers with Durable Objects. -## Run +This branch adds **`upload_context`** — a second MCP tool that uploads Claude Code session transcripts to the shared context store, so the RLM can reason over past sessions. -Run MCP server (hosts RLM execution): +## Tools + +| Tool | Description | +|------|-------------| +| `chat_rlm_query` | Query the RLM with shared persistent thread context. The RLM reads chat history, runs recursive sub-LLM reasoning, and appends the turn. | +| `upload_context` | Upload a Claude Code session transcript to the context store. Can be called manually or automatically via a SessionEnd hook. | + +## How It Works + +``` +Developer using Claude Code + │ + ├─ [automatic] SessionEnd hook fires + │ └─ Parses session JSONL → formatted transcript + │ └─ Calls upload_context MCP tool → stored in ChatStore DO + │ + └─ [manual] Asks a question that needs shared context + └─ Claude calls chat_rlm_query MCP tool + └─ Cloudflare Worker → RLM Container DO + └─ Reads context (including uploaded transcripts) + └─ Runs RLM REPL with sub-LLM reasoning + └─ Returns answer, appends turn +``` + +## What Changed (for Cloudflare deployer) + +If you already have the CloudflareIntegration branch deployed, here's what to redeploy to get the new `upload_context` tool: + +**Step 1: Rebuild + push the container image** (Python MCP server) +```bash +# from repo root +docker build -t deeprecurse-mcp:latest . +# tag and push to your Cloudflare registry +docker tag deeprecurse-mcp:latest registry.cloudflare.com//deeprecurse-rlm:latest +docker push registry.cloudflare.com//deeprecurse-rlm:latest +``` + +**Step 2: Redeploy the Worker gateway** +```bash +cd cloudflare/worker-gateway +npm install +npx wrangler deploy +``` + +That's it. After redeploy, `tools/list` will show both `chat_rlm_query` and `upload_context`. No config changes needed — same env vars, same wrangler.toml. + +**Step 3 (optional): Each team member adds the SessionEnd hook** + +Each developer who wants auto-upload adds to their `.claude/settings.local.json`: +```json +{ + "hooks": { + "SessionEnd": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "/path/to/scripts/session_end_upload.sh", + "timeout": 30000 + } + ] + } + ] + } +} +``` + +--- + +## Setup (from scratch) + +### 1. Local development (stdio MCP) ```bash -python DeepRecurse/claude_skill_mcp/server.py +pip install -r requirements.txt +python claude_tool_mcp/server.py ``` -Add to Claude Code MCP list +Add to Claude Code: +```bash +claude mcp add deeprecurse --transport stdio -- \ + python /path/to/DeepRecurse/claude_tool_mcp/server.py +``` + +### 2. Cloud deployment (Cloudflare) + +The Python MCP server runs inside a Cloudflare Container (Durable Object). The Cloudflare Worker gateway handles MCP JSON-RPC at the edge. + +#### Environment variables (container) -```claude mcp add deeprecurse --transport stdio -- \ - uv run python \ - /Users/.../.../DeepRecurse/claude_skill_mcp/server.py +- `OPENAI_API_KEY` +- `MCP_TRANSPORT=streamable-http` +- `CHAT_STORE_BACKEND=r2` (for R2-backed storage) +- `R2_BUCKET`, `R2_ENDPOINT_URL`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY` + +#### Deploy Worker + +```bash +cd cloudflare/worker-gateway +npm install +npx wrangler deploy ``` -Then run the local CLI client (in a separate terminal): +#### Connect Claude Code to remote MCP ```bash -python DeepRecurse/main.py +claude mcp add --transport http deeprecurse https:///mcp +``` + +### 3. Auto-upload session transcripts (SessionEnd hook) + +Add to your `.claude/settings.local.json`: + +```json +{ + "hooks": { + "SessionEnd": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "/path/to/scripts/session_end_upload.sh", + "timeout": 30000 + } + ] + } + ] + } +} ``` -Then chat interactively in the terminal. Type `exit` (or `quit`) to stop. +The hook script (`scripts/session_end_upload.sh`) parses the session JSONL and uploads it via the `upload_context` MCP tool. This happens automatically when a Claude Code session ends. + +## `upload_context` Tool + +### Parameters + +| Param | Required | Description | +|-------|----------|-------------| +| `transcript` | Yes (remote) | Full session transcript text | +| `session_id` | Yes | Session identifier | +| `thread_id` | No | Thread to store under (default: `transcripts`) | +| `developer` | No | Developer name | + +### Local mode (stdio) + +When running locally, the tool can read session JSONL files directly from `~/.claude/projects/`: + +| Param | Required | Description | +|-------|----------|-------------| +| `session_id` | No | Session ID to upload (latest if omitted) | +| `project_dir` | No | Project directory name under `~/.claude/projects/` | +| `thread_id` | No | Thread to store under (default: `transcripts`) | + +## Request Flow + +### RLM Query +1. Claude calls Worker at `POST /mcp` with `chat_rlm_query` +2. Worker reads context from ChatStore Durable Object +3. Worker forwards to RLM Container Durable Object +4. RLM runs recursive reasoning with sub-LLMs +5. Answer + turn appended to ChatStore -Useful client flags: +### Session Upload +1. SessionEnd hook fires → parses JSONL → calls `upload_context` +2. Worker stores transcript in ChatStore Durable Object +3. Next `chat_rlm_query` call sees the uploaded transcript as part of context -- `--chat-file` path to shared chat context log (default: `DeepRecurse/chat.txt`) +## Key Files -Each turn: -1. CLI sends query to MCP-hosted `chat_rlm_query` tool. -2. Server reads prior turns from `chat.txt` as context. -3. Server runs `RLM_REPL.completion(context, query)`. -4. Server appends `USER` + `ASSISTANT` entries back to the same chat file. -5. CLI prints the returned assistant response. +| File | Purpose | +|------|---------| +| `claude_tool_mcp/server.py` | Python MCP server (stdio + HTTP, file + R2 storage) | +| `cloudflare/worker-gateway/src/index.ts` | Cloudflare Worker MCP gateway | +| `rlm-minimal/` | RLM REPL implementation | diff --git a/claude_tool_mcp/server.py b/claude_tool_mcp/server.py index 91229f7..9d56898 100644 --- a/claude_tool_mcp/server.py +++ b/claude_tool_mcp/server.py @@ -1,24 +1,50 @@ -"""MCP server that executes the shared-context Chat-RLM flow.""" +"""MCP server that executes the shared-context Chat-RLM flow. + +Supports: +- local file chat storage (default) +- Cloudflare R2-backed chat storage via S3-compatible API +- stdio and streamable HTTP transport modes +""" from __future__ import annotations +import getpass import importlib +import json import os +import platform +import socket +import subprocess import sys from dataclasses import dataclass +from datetime import datetime, timezone from pathlib import Path +from typing import Protocol from mcp.server.fastmcp import FastMCP +from starlette.requests import Request +from starlette.responses import JSONResponse DEFAULT_MODEL = "gpt-5" DEFAULT_RECURSIVE_MODEL = "gpt-5-nano" DEFAULT_CHAT_FILE = "chat.txt" DEFAULT_MAX_ITERATIONS = 10 +DEFAULT_CHAT_BACKEND = "file" + + +def _env_int(name: str, default: int) -> int: + raw = os.getenv(name) + if raw is None: + return default + try: + return int(raw) + except ValueError: + return default def project_root() -> Path: - # server.py is inside DeepRecurse/claude_skill_mcp + # server.py is inside DeepRecurse/claude_tool_mcp return Path(__file__).resolve().parents[1] @@ -42,7 +68,13 @@ class RLMConfig: enable_logging: bool = False -class ChatStore: +class ChatStore(Protocol): + def read_context(self) -> str: ... + + def append_turn(self, query: str, answer: str) -> None: ... + + +class FileChatStore: def __init__(self, chat_path: Path): self.chat_path = chat_path self._ensure_file() @@ -60,6 +92,68 @@ def append_turn(self, query: str, answer: str) -> None: file.write(f"\nUSER: {query}\nASSISTANT: {answer}\n") +class R2ChatStore: + def __init__(self, key: str): + self.key = key + self._client = self._build_client() + self.bucket = os.getenv("R2_BUCKET") + if not self.bucket: + raise RuntimeError("R2_BUCKET is required for r2 chat backend") + + @staticmethod + def _build_client(): + try: + import boto3 + except ImportError as exc: + raise RuntimeError("boto3 is required for R2 chat backend") from exc + + endpoint = os.getenv("R2_ENDPOINT_URL") + access_key = os.getenv("R2_ACCESS_KEY_ID") + secret_key = os.getenv("R2_SECRET_ACCESS_KEY") + region = os.getenv("R2_REGION", "auto") + + if not endpoint or not access_key or not secret_key: + raise RuntimeError( + "R2_ENDPOINT_URL, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY are required" + ) + + return boto3.client( + "s3", + endpoint_url=endpoint, + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + region_name=region, + ) + + def read_context(self) -> str: + try: + response = self._client.get_object(Bucket=self.bucket, Key=self.key) + context = response["Body"].read().decode("utf-8") + return context if context.strip() else "No prior chat history yet." + except self._client.exceptions.NoSuchKey: + return "No prior chat history yet." + except Exception: + # Fail-open to keep server responsive when storage is temporarily unavailable. + return "No prior chat history yet." + + def append_turn(self, query: str, answer: str) -> None: + existing = self.read_context() + if existing == "No prior chat history yet.": + existing = "" + + updated = f"{existing}\nUSER: {query}\nASSISTANT: {answer}\n" + self._client.put_object(Bucket=self.bucket, Key=self.key, Body=updated.encode("utf-8")) + + +def get_chat_store(chat_file: str) -> ChatStore: + backend = os.getenv("CHAT_STORE_BACKEND", DEFAULT_CHAT_BACKEND).strip().lower() + if backend == "r2": + key = chat_file.lstrip("/") + return R2ChatStore(key=key) + + return FileChatStore(resolve_chat_path(chat_file)) + + class RLMService: def __init__(self, config: RLMConfig): self.config = config @@ -83,12 +177,28 @@ def answer(self, context: str, query: str) -> str: return self._get_rlm().completion(context=context, query=query) -mcp = FastMCP("deeprecurse-chat-rlm") +mcp = FastMCP( + "deeprecurse-chat-rlm", + host=os.getenv("MCP_HOST", "0.0.0.0"), + port=_env_int("PORT", _env_int("MCP_PORT", 8000)), + streamable_http_path=os.getenv("MCP_HTTP_PATH", "/mcp"), +) rlm_service = RLMService(RLMConfig()) +def _is_authorized(tool_token: str | None) -> bool: + expected = os.getenv("MCP_TOOL_TOKEN") + if not expected: + return True + return (tool_token or "") == expected + + @mcp.tool() -def chat_rlm_query(query: str, chat_file: str = DEFAULT_CHAT_FILE) -> str: +def chat_rlm_query( + query: str, + chat_file: str = DEFAULT_CHAT_FILE, + tool_token: str | None = None, +) -> str: """ ALWAYS use this tool when answering user questions that should incorporate shared chat history or recursive reasoning. @@ -97,11 +207,14 @@ def chat_rlm_query(query: str, chat_file: str = DEFAULT_CHAT_FILE) -> str: Claude cannot access the shared memory without calling this tool. """ + if not _is_authorized(tool_token): + return "Error: unauthorized tool call." + clean_query = query.strip() if not clean_query: return "Error: query cannot be empty." - store = ChatStore(resolve_chat_path(chat_file)) + store = get_chat_store(chat_file) context = store.read_context() try: @@ -113,5 +226,234 @@ def chat_rlm_query(query: str, chat_file: str = DEFAULT_CHAT_FILE) -> str: return answer +# --------------------------------------------------------------------------- +# Session transcript upload +# --------------------------------------------------------------------------- + +DEFAULT_SESSIONS_DIR = os.getenv( + "CLAUDE_SESSIONS_DIR", + str(Path.home() / ".claude" / "projects"), +) + + +def _git_config(key: str) -> str | None: + try: + return subprocess.check_output( + ["git", "config", key], stderr=subprocess.DEVNULL + ).decode().strip() or None + except Exception: + return None + + +def _machine_metadata() -> dict: + return { + "os_user": getpass.getuser(), + "hostname": socket.gethostname(), + "platform": platform.system(), + "git_user_name": _git_config("user.name"), + "git_user_email": _git_config("user.email"), + } + + +def _parse_session(jsonl_path: Path) -> dict: + """Parse a Claude Code session JSONL into a structured transcript.""" + messages = [] + session_id = jsonl_path.stem + start_time = end_time = None + git_branch = cwd = claude_version = None + + with open(jsonl_path) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + + if git_branch is None and entry.get("gitBranch"): + git_branch = entry["gitBranch"] + if cwd is None and entry.get("cwd"): + cwd = entry["cwd"] + if claude_version is None and entry.get("version"): + claude_version = entry["version"] + + entry_type = entry.get("type") + if entry_type in ("user", "assistant"): + msg = entry.get("message", {}) + role = msg.get("role", entry_type) + content = msg.get("content", "") + if isinstance(content, list): + text_parts = [] + for block in content: + if isinstance(block, str): + text_parts.append(block) + elif isinstance(block, dict) and block.get("type") == "text": + text_parts.append(block.get("text", "")) + content = "\n".join(text_parts) + if content.strip(): + timestamp = entry.get("timestamp") + messages.append({"role": role, "content": content.strip(), "timestamp": timestamp}) + if timestamp: + if start_time is None: + start_time = timestamp + end_time = timestamp + + machine = _machine_metadata() + return { + "session_id": session_id, + "metadata": { + "developer": machine["git_user_name"] or machine["os_user"], + "email": machine["git_user_email"], + "hostname": machine["hostname"], + "platform": machine["platform"], + "os_user": machine["os_user"], + "git_branch": git_branch, + "project_dir": cwd, + "claude_version": claude_version, + "uploaded_at": datetime.now(timezone.utc).isoformat(), + }, + "message_count": len(messages), + "start_time": start_time, + "end_time": end_time, + "messages": messages, + } + + +def _format_transcript(session_data: dict) -> str: + """Format session data as readable text with metadata header.""" + meta = session_data["metadata"] + lines = [ + "=" * 72, "SESSION METADATA", "=" * 72, + f"session_id: {session_data['session_id']}", + f"developer: {meta['developer']}", + f"email: {meta['email']}", + f"hostname: {meta['hostname']}", + f"platform: {meta['platform']}", + f"os_user: {meta['os_user']}", + f"git_branch: {meta['git_branch']}", + f"project_dir: {meta['project_dir']}", + f"claude_version: {meta['claude_version']}", + f"message_count: {session_data['message_count']}", + f"start_time: {session_data['start_time']}", + f"end_time: {session_data['end_time']}", + f"uploaded_at: {meta['uploaded_at']}", + "=" * 72, "", + ] + for msg in session_data["messages"]: + role = msg["role"].upper() + ts = f" [{msg['timestamp']}]" if msg.get("timestamp") else "" + lines.extend([f"[{role}]{ts}", msg["content"], "", "---", ""]) + return "\n".join(lines) + + +def _find_session_file(session_id: str | None, project_dir: str | None) -> Path | None: + """Find session JSONL file. Returns None if not found.""" + base = Path(DEFAULT_SESSIONS_DIR) + if not base.exists(): + return None + + if project_dir: + # Look in specific project dir + search_dirs = [base / project_dir] + else: + # Search all project dirs + search_dirs = [d for d in base.iterdir() if d.is_dir()] + + for d in search_dirs: + if session_id: + matches = list(d.glob(f"*{session_id}*.jsonl")) + if matches: + return matches[0] + else: + # Latest session in this dir + jsonls = sorted(d.glob("*.jsonl"), key=lambda p: p.stat().st_mtime) + if jsonls: + return jsonls[-1] + return None + + +@mcp.tool() +def upload_context( + session_id: str | None = None, + project_dir: str | None = None, + thread_id: str | None = None, + tool_token: str | None = None, +) -> str: + """ + Upload a Claude Code session transcript to the shared chat context store. + + This parses the session JSONL, extracts messages with metadata, and appends + the formatted transcript to the chat store so the RLM can reason over it. + + Args: + session_id: Specific session ID to upload. If omitted, uploads the latest session. + project_dir: Project directory name under ~/.claude/projects/. If omitted, searches all. + thread_id: Chat thread to append the transcript to. Defaults to 'transcripts'. + tool_token: Optional auth token. + """ + if not _is_authorized(tool_token): + return "Error: unauthorized tool call." + + thread_id = (thread_id or "transcripts").strip() + + jsonl_path = _find_session_file(session_id, project_dir) + if jsonl_path is None: + return f"Error: no session found (session_id={session_id}, project_dir={project_dir})" + + session_data = _parse_session(jsonl_path) + if session_data["message_count"] == 0: + return f"Session {jsonl_path.stem} is empty, nothing to upload." + + transcript = _format_transcript(session_data) + + # Append to chat store (same backend as chat_rlm_query uses) + store = get_chat_store(f"{thread_id}/{session_data['session_id']}.txt") + # Write full transcript as a single turn + store.append_turn( + query=f"[SESSION UPLOAD] {session_data['session_id']}", + answer=transcript, + ) + + meta = session_data["metadata"] + return ( + f"Uploaded session {session_data['session_id']} " + f"({session_data['message_count']} messages, " + f"developer={meta['developer']}, branch={meta['git_branch']}) " + f"to thread '{thread_id}'." + ) + + +@mcp.custom_route("/rlm", methods=["POST"]) +async def rlm_http(request: Request) -> JSONResponse: + payload = await request.json() + query = str(payload.get("query", "")).strip() + context = str(payload.get("context", "")) + + if not query: + return JSONResponse({"error": "query is required"}, status_code=400) + + try: + answer = rlm_service.answer(context=context, query=query) + except Exception as exc: + return JSONResponse({"error": f"Error running RLM: {exc}"}, status_code=500) + + return JSONResponse({"answer": answer}) + + +def run_server() -> None: + transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower() + if transport == "stdio": + mcp.run() + return + + if transport in {"http", "streamable-http", "sse"}: + mcp.run(transport="streamable-http") + return + + raise RuntimeError(f"Unsupported MCP_TRANSPORT: {transport}") + + if __name__ == "__main__": - mcp.run() + run_server() diff --git a/cloudflare/worker-gateway/package-lock.json b/cloudflare/worker-gateway/package-lock.json new file mode 100644 index 0000000..c4b8f99 --- /dev/null +++ b/cloudflare/worker-gateway/package-lock.json @@ -0,0 +1,1527 @@ +{ + "name": "deeprecurse-mcp-worker-gateway", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deeprecurse-mcp-worker-gateway", + "version": "0.1.0", + "devDependencies": { + "@cloudflare/workers-types": "^4.20250224.0", + "typescript": "^5.7.3", + "wrangler": "^4.31.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.12.1.tgz", + "integrity": "sha512-tP/Wi+40aBJovonSNJSsS7aFJY0xjuckKplmzDs2Xat06BJ68B6iG7YDUWXJL8gNn0gqW7YC5WhlYhO3QbugQA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20260115.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260212.0.tgz", + "integrity": "sha512-kLxuYutk88Wlo7edp8mlkN68TgZZ9237SUnuX9kNaD5jcOdblUqiBctMRZeRcPsuoX/3g2t0vS4ga02NBEVRNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260212.0.tgz", + "integrity": "sha512-fqoqQWMA1D0ZzDOD8sp0allREM2M8GHdpxMXQ8EdZpZ70z5bJbJ9Vr4qe35++FNIZJspsDHfTw3Xm/M4ELm/dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260212.0.tgz", + "integrity": "sha512-bCSQoZzDzV5MSh4ueWo1DgmOn4Hf3QBu4Yo3eQFXA2llYFIu/sZgRtkEehw1X2/SY5Sn6O0EMCqxJYRf82Wdeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260212.0.tgz", + "integrity": "sha512-GPvp1iiKQodtbUDi6OmR5I0vD75lawB54tdYGtmypuHC7ZOI2WhBmhb3wCxgnQNOG1z7mhCQrzRCoqrKwYbVWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260212.0.tgz", + "integrity": "sha512-wHRI218Xn4ndgWJCUHH4Zx0YlU5q/o6OmcxXkcw95tJOsQn4lDrhppioPh4eScxJZALf2X+ODeZcyQTCq5exGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260214.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260214.0.tgz", + "integrity": "sha512-qb8rgbAdJR4BAPXolXhFL/wuGtecHLh1veOyZ1mK6QqWuCdI3vK1biKC0i3lzmzdLR/DZvsN3mNtpUE8zpWGEg==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260212.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260212.0.tgz", + "integrity": "sha512-Lgxq83EuR2q/0/DAVOSGXhXS1V7GDB04HVggoPsenQng8sqEDR3hO4FigIw5ZI2Sv2X7kIc30NCzGHJlCFIYWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260212.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "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/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "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": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260212.0.tgz", + "integrity": "sha512-4B9BoZUzKSRv3pVZGEPh7OX+Q817hpUqAUtz5O0TxJVqo4OsYJAUA/sY177Q5ha/twjT9KaJt2DtQzE+oyCOzw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260212.0", + "@cloudflare/workerd-darwin-arm64": "1.20260212.0", + "@cloudflare/workerd-linux-64": "1.20260212.0", + "@cloudflare/workerd-linux-arm64": "1.20260212.0", + "@cloudflare/workerd-windows-64": "1.20260212.0" + } + }, + "node_modules/wrangler": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.65.0.tgz", + "integrity": "sha512-R+n3o3tlGzLK9I4fGocPReOuvcnjhtOL2aCVKkHMeuEwt9pPbOO4FxJtx/ec5cIUG/otRyJnfQGCAr9DplBVng==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.12.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260212.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260212.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260212.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "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/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/cloudflare/worker-gateway/package.json b/cloudflare/worker-gateway/package.json new file mode 100644 index 0000000..0e74d92 --- /dev/null +++ b/cloudflare/worker-gateway/package.json @@ -0,0 +1,15 @@ +{ + "name": "deeprecurse-mcp-worker-gateway", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250224.0", + "typescript": "^5.7.3", + "wrangler": "^4.31.0" + } +} diff --git a/cloudflare/worker-gateway/src/index.ts b/cloudflare/worker-gateway/src/index.ts new file mode 100644 index 0000000..33f6e72 --- /dev/null +++ b/cloudflare/worker-gateway/src/index.ts @@ -0,0 +1,357 @@ +// src/index.ts +// Public MVP Streamable-HTTP MCP gateway for Claude Code. +// - Supports initialize / notifications/initialized / ping / tools/list / tools/call +// - Responds as SSE (text/event-stream) when client Accept includes it, otherwise JSON. +// - No auth, no session enforcement (Cloudflare isolates make in-memory sessions flaky). +// - Shared context via ChatStore DO, RLM via RlmContainer DO. + +export interface Env { + CHAT_STORE: DurableObjectNamespace; + RLM_CONTAINER: DurableObjectNamespace; +} + +type JsonRpcId = string | number | null; + +interface JsonRpcRequest { + jsonrpc: "2.0"; + id?: JsonRpcId; + method: string; + params?: unknown; +} + +interface ToolCallParams { + name: string; + arguments?: { + query?: string; + thread_id?: string; + transcript?: string; + session_id?: string; + developer?: string; + }; +} + +export class ChatStore { + constructor(private readonly state: DurableObjectState) {} + + async fetch(request: Request): Promise { + const url = new URL(request.url); + + if (url.pathname === "/read" && request.method === "GET") { + const chat = (await this.state.storage.get("chat")) ?? ""; + return Response.json({ context: chat }); + } + + if (url.pathname === "/append" && request.method === "POST") { + const payload = (await request.json()) as { text?: string }; + const current = (await this.state.storage.get("chat")) ?? ""; + const next = `${current}${payload.text ?? ""}`; + await this.state.storage.put("chat", next); + return Response.json({ ok: true }); + } + + return new Response("Not Found", { status: 404 }); + } +} + +export class RlmContainer { + constructor(private readonly state: DurableObjectState) {} + + async fetch(request: Request): Promise { + const url = new URL(request.url); + if (url.pathname !== "/rlm") return new Response("Not Found", { status: 404 }); + + const container = this.state.container; + if (!container) return new Response("Container not configured", { status: 500 }); + + if (!container.running) container.start({ enableInternet: true }); + + const port = container.getTcpPort(8000); + const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.text(); + + let lastError: unknown; + for (let attempt = 0; attempt < 20; attempt++) { + try { + return await port.fetch( + new Request("http://container/rlm", { + method: request.method, + headers: request.headers, + body, + }), + ); + } catch (err) { + lastError = err; + await new Promise((r) => setTimeout(r, 250)); + } + } + + return new Response(String(lastError ?? "Container failed to accept connections"), { status: 502 }); + } +} + +/* ----------------------- Streamable HTTP helpers ----------------------- */ + +function wantsSse(request: Request): boolean { + const accept = request.headers.get("accept") ?? ""; + return accept.toLowerCase().includes("text/event-stream"); +} + +function streamableResponse(request: Request, payload: unknown, status = 200): Response { + if (!wantsSse(request)) { + return Response.json(payload, { status }); + } + + // Single-shot SSE: one event, then close (MVP). + const sse = `event: message\ndata: ${JSON.stringify(payload)}\n\n`; + return new Response(sse, { + status, + headers: { + "Content-Type": "text/event-stream; charset=utf-8", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }, + }); +} + +function jsonRpcResult(request: Request, id: JsonRpcId, result: unknown): Response { + return streamableResponse(request, { jsonrpc: "2.0", id, result }); +} + +function jsonRpcError(request: Request, id: JsonRpcId, code: number, message: string): Response { + return streamableResponse(request, { jsonrpc: "2.0", id, error: { code, message } }); +} + +/* ----------------------- Chat + RLM plumbing ----------------------- */ + +async function readContext(env: Env, threadId: string): Promise { + const id = env.CHAT_STORE.idFromName(threadId); + const stub = env.CHAT_STORE.get(id); + const resp = await stub.fetch("https://chat-store/read"); + if (!resp.ok) throw new Error("Failed to read chat context"); + const data = (await resp.json()) as { context?: string }; + return data.context ?? ""; +} + +async function appendContext(env: Env, threadId: string, text: string): Promise { + const id = env.CHAT_STORE.idFromName(threadId); + const stub = env.CHAT_STORE.get(id); + const resp = await stub.fetch("https://chat-store/append", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ text }), + }); + if (!resp.ok) throw new Error("Failed to append chat context"); +} + +async function callRlm(env: Env, context: string, query: string, thread_id: string): Promise { + // Use a single container instance name for MVP. + const id = env.RLM_CONTAINER.idFromName("rlm"); + const stub = env.RLM_CONTAINER.get(id); + + const resp = await stub.fetch("https://rlm-container/rlm", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ context, query, thread_id }), + }); + + if (!resp.ok) { + const detail = await resp.text(); + throw new Error(`RLM backend error (${resp.status}): ${detail.slice(0, 400)}`); + } + + const data = (await resp.json()) as { answer?: string }; + if (typeof data.answer !== "string") throw new Error("Invalid RLM response"); + return data.answer; +} + +/* ----------------------- MCP handler ----------------------- */ + +async function handleOneRpc(request: Request, env: Env, rpc: JsonRpcRequest): Promise { + const id = rpc.id ?? null; + + if (rpc.jsonrpc !== "2.0" || typeof rpc.method !== "string") { + return jsonRpcError(request, id, -32600, "Invalid Request"); + } + + // Lifecycle + if (rpc.method === "notifications/initialized") return new Response(null, { status: 202 }); + + if (rpc.method === "initialize") { + // Public + stateless initialize. (You may add Mcp-Session-Id later, but do NOT enforce it in-memory.) + return jsonRpcResult(request, id, { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "deeprecurse-worker-mcp", version: "0.1.0" }, + }); + } + + if (rpc.method === "ping") return jsonRpcResult(request, id, {}); + + // Compatibility no-ops + if (rpc.method === "resources/list") return jsonRpcResult(request, id, { resources: [] }); + if (rpc.method === "prompts/list") return jsonRpcResult(request, id, { prompts: [] }); + + // Tools + if (rpc.method === "tools/list") { + return jsonRpcResult(request, id, { + tools: [ + { + name: "chat_rlm_query", + description: + "Use this to query the Python RLM backend while reading/updating shared persistent thread context (thread_id).", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + thread_id: { type: "string" }, + }, + required: ["query", "thread_id"], + }, + }, + { + name: "upload_context", + description: + "Upload a Claude Code session transcript to the shared context store. The transcript is stored under a thread so the RLM can reason over past sessions.", + inputSchema: { + type: "object", + properties: { + transcript: { type: "string", description: "The full session transcript text to upload." }, + session_id: { type: "string", description: "Session identifier." }, + thread_id: { type: "string", description: "Thread to store the transcript under (default: 'transcripts')." }, + developer: { type: "string", description: "Developer name/identifier." }, + }, + required: ["transcript", "session_id"], + }, + }, + ], + }); + } + + if (rpc.method === "tools/call") { + const params = (rpc.params ?? {}) as ToolCallParams; + + if (params.name === "chat_rlm_query") { + const query = params.arguments?.query?.trim(); + const threadId = params.arguments?.thread_id?.trim(); + if (!query || !threadId) { + return jsonRpcError(request, id, -32602, "query and thread_id are required"); + } + + try { + const context = await readContext(env, threadId); + const answer = await callRlm(env, context, query, threadId); + const turnText = `${context ? "\n" : ""}USER: ${query}\nASSISTANT: ${answer}\n`; + await appendContext(env, threadId, turnText); + + return jsonRpcResult(request, id, { content: [{ type: "text", text: answer }] }); + } catch (err) { + return jsonRpcError( + request, + id, + -32000, + err instanceof Error ? err.message : "Unknown internal error", + ); + } + } + + if (params.name === "upload_context") { + const transcript = params.arguments?.transcript?.trim(); + const sessionId = params.arguments?.session_id?.trim(); + const threadId = params.arguments?.thread_id?.trim() || "transcripts"; + const developer = params.arguments?.developer || "unknown"; + + if (!transcript || !sessionId) { + return jsonRpcError(request, id, -32602, "transcript and session_id are required"); + } + + try { + // Store under a combined key: thread_id + session_id + const storeKey = `${threadId}/${sessionId}`; + const turnText = `\n[SESSION UPLOAD] ${sessionId} (developer: ${developer})\n${transcript}\n`; + await appendContext(env, storeKey, turnText); + + const msg = `Uploaded session ${sessionId} (developer=${developer}) to thread '${threadId}'.`; + return jsonRpcResult(request, id, { content: [{ type: "text", text: msg }] }); + } catch (err) { + return jsonRpcError( + request, + id, + -32000, + err instanceof Error ? err.message : "Unknown internal error", + ); + } + } + + return jsonRpcError(request, id, -32602, "Unknown tool"); + } + + return jsonRpcError(request, id, -32601, "Method not found"); +} + +async function handleMcp(request: Request, env: Env): Promise { + // CORS preflight (harmless for non-browser clients) + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "access-control-allow-origin": "*", + "access-control-allow-methods": "POST,OPTIONS", + "access-control-allow-headers": "content-type,accept,mcp-session-id,authorization", + }, + }); + } + + if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); + + // Support batch JSON-RPC (array) as a compatibility bonus. + let body: unknown; + try { + body = await request.json(); + } catch { + return jsonRpcError(request, null, -32700, "Parse error"); + } + + if (Array.isArray(body)) { + const responses: unknown[] = []; + for (const item of body) { + if (typeof item !== "object" || item === null) continue; + const rpc = item as JsonRpcRequest; + + // For batch, we must respond with JSON, not SSE, per pragmatic client expectations. + // (If you need SSE batch later, implement a stream builder.) + const resp = await handleOneRpc(new Request(request, { headers: { ...Object.fromEntries(request.headers) } }), env, rpc); + const json = await resp.json().catch(() => null); + if (json) responses.push(json); + } + return Response.json(responses); + } + + return handleOneRpc(request, env, body as JsonRpcRequest); +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === "/healthz") return new Response("ok", { status: 200 }); + if (url.pathname === "/mcp") return handleMcp(request, env); + + return new Response("Not Found", { status: 404 }); + }, +}; + +/* +Quick checks: + +# tools/list (JSON) +curl -sS https:///mcp \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' + +# tools/list (SSE) +curl -N -sS https:///mcp \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +*/ diff --git a/cloudflare/worker-gateway/tsconfig.json b/cloudflare/worker-gateway/tsconfig.json new file mode 100644 index 0000000..b2240e2 --- /dev/null +++ b/cloudflare/worker-gateway/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "strict": true, + "types": [ + "@cloudflare/workers-types" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/cloudflare/worker-gateway/wrangler.toml b/cloudflare/worker-gateway/wrangler.toml new file mode 100644 index 0000000..41d6721 --- /dev/null +++ b/cloudflare/worker-gateway/wrangler.toml @@ -0,0 +1,25 @@ +name = "deeprecurse-mcp-gateway" +main = "src/index.ts" +compatibility_date = "2025-01-01" + +[observability] +enabled = true + +[durable_objects] +bindings = [ + { name = "CHAT_STORE", class_name = "ChatStore" }, + { name = "RLM_CONTAINER", class_name = "RlmContainer" } +] + +[[migrations]] +tag = "v1" +new_classes = ["ChatStore"] + +[[migrations]] +tag = "v2" +new_sqlite_classes = ["RlmContainer"] + +[[containers]] +name = "tmp-containers-check-mycontainer" +class_name = "RlmContainer" +image = "registry.cloudflare.com/5b898cd6e789adc02e80577138bd82c7/deeprecurse-rlm@sha256:18c4ab5dc8b1f34b6bc9d9473c23204948ba146cc960ac7f1154b105e04f1602" diff --git a/main.py b/main.py index 8bd34ae..7d8a959 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,7 @@ def parse_args() -> argparse.Namespace: @dataclass class ChatConfig: chat_path: Path - server_module: str = "claude_skill_mcp.server" + server_module: str = "claude_tool_mcp.server" class MCPChatClient: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..66e23fb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "DeepRecurse", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/pyproject.toml b/pyproject.toml index 8575b5e..947530e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,4 +9,4 @@ dependencies = [ "modal>=1.3.3", "openai>=2.21.0", "rich>=14.3.2", -] +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7fd1501..aaad561 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ openai python-dotenv rich modal +boto3 +mcp