diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8425006 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,49 @@ +# Dependencies + +node_modules/ +.yarn/ + +# Build outputs + +dist/ + +# Development files + +\*.log +.DS_Store + +# IDE + +.vscode/ +.idea/ + +# Test files + +tests/ + +# Cache files + +.gemini-cache.json + +# Git + +.git/ +.gitignore + +# Documentation (not needed in image) + +\*.md +!README.md + +# Dev dependencies files + +.prettierrc +.prettierignore + +# GitHub workflows + +.github/ + +# K8s config + +k8s.yaml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..aeff4d0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ + +calcit.cirru -diff linguist-generated +yarn.lock -diff linguist-generated +Cargo.lock -diff linguist-generated +lib -diff linguist-generated diff --git a/.github/workflows/docker-pr.yaml b/.github/workflows/docker-pr.yaml new file mode 100644 index 0000000..bb0ea1b --- /dev/null +++ b/.github/workflows/docker-pr.yaml @@ -0,0 +1,65 @@ +name: Docker Build on PR + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "src/**" + - "package.json" + - "yarn.lock" + - "tsconfig.json" + - "Dockerfile" + - ".dockerignore" + - "store/**" + - "source/**" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=pr + type=sha,prefix=pr- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Set retention policy + run: | + # This is a placeholder for setting retention policy + # GitHub Container Registry doesn't support direct retention policy via API + # The retention will be handled by GitHub's built-in retention policies + echo "Retention policy: PR images will be retained for 7 days" diff --git a/.github/workflows/docker-release.yaml b/.github/workflows/docker-release.yaml new file mode 100644 index 0000000..5689a0e --- /dev/null +++ b/.github/workflows/docker-release.yaml @@ -0,0 +1,58 @@ +name: Docker Build on Release + +on: + release: + types: [published] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Set retention policy + run: | + # Release images are kept indefinitely by default + # This step documents the retention policy + echo "Retention policy: Release images will be retained for at least 1 year" diff --git a/.gitignore b/.gitignore index 397b0d9..1203a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,10 @@ dist/ *.log .env .DS_Store + +source/ + +# Gemini file cache +.gemini-cache.json +# K8s secrets (contains API keys) +k8s-secrets.yaml \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b96da80 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +dist/ +node_modules/ +.gemini-cache.json +*.md +source/ +store/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ace6e02 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "arrowParens": "always" +} diff --git a/1 b/1 new file mode 100644 index 0000000..e69de29 diff --git a/APIs.md b/APIs.md new file mode 100644 index 0000000..b927ee6 --- /dev/null +++ b/APIs.md @@ -0,0 +1,49 @@ +# Moonverse HTTP API + +A minimal JSON API that replaces the previous MCP server. Supports async query tasks with polling. All responses are JSON. + +## Endpoints + +### Health + +- `GET /healthz` +- **Response**: `{ "ok": true }` + +### Create query task + +- `POST /query` +- **Body**: + +```json +{ "type": "docs" | "source", "query": "" } +``` + +- **Responses**: + - `202 Accepted`: `{ "id": "", "nextPollSec": 2 }` + - `400 Bad Request`: `{ "error": "..." }` + +### Get query task status/result + +- `GET /query/{id}` +- **Responses**: + - In progress: `{ "status": "queued" | "running", "etaSeconds": , "nextPollSec": 2, "message": "Processing in background. Please poll again." }` + - Completed: `{ "status": "done", "content": "" }` + - Error: `{ "status": "error", "error": "" }` + - Not found: `404 { "error": "task not found" }` + +## Behavior + +- Tasks execute asynchronously; clients should poll `GET /query/{id}` every ~2s using `nextPollSec` as a hint. +- Queries run against uploaded MoonBit files: `type=docs` uses `store/` files, `type=source` uses `source/` files. +- Supported file types for upload are `.md`, `.mbt`, `.json`, `.txt`. +- The server streams nothing; results are returned when ready. + +## Environment + +- `GEMINI_API_KEY` (required): Google Generative AI key. +- `PORT` (optional): defaults to 8080. + +## Notes + +- Health endpoint is for readiness checks. +- Ensure `store/` and `source/` folders exist with allowed files; otherwise responses will explain missing files. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e507e1f..603e6db 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,176 +1,329 @@ -# Architecture - -## Overview - -Moonverse.ts is a Model Context Protocol (MCP) server that provides AI-powered search over MoonBit documentation and source code using Google's Gemini Interactions API. - -## Components - -### 1. MCP Server (`MoonverseMCPServer`) - -The main server class that implements the MCP protocol using the `@modelcontextprotocol/sdk`. - -**Key Responsibilities:** -- Manage server lifecycle -- Handle tool registration and requests (two tools: docs and source) -- Coordinate between file management and query processing -- Maintain separate caches for documentation and source files - -### 2. File Management - -Uses Google's `GoogleAIFileManager` to upload and manage both documentation and source code files. +# 架构设计 -**Flow:** -1. Scan `store/` directory for documentation files (`.txt`, `.md`) -2. Scan `source/` directory recursively for source files (`.mbt`, `.md`, `.json`, `.txt`) -3. Upload files lazily on first query to Gemini's Interactions API -4. Store file metadata in separate lists for documentation and source code -5. Cache uploaded files in memory to avoid re-uploads +## 概述 -### 3. Query Processing +Moonverse.ts 是一个基于 Google Gemini API 的 MoonBit 文档和源码搜索服务,提供 HTTP JSON API。 -Handles two types of queries using Gemini's Interactions API with uploaded files as context. +## 系统架构 -**Documentation Queries (`query_moonbit_docs`):** -- Uses files from `store/` directory -- Focuses on answering based on official documentation -- Provides user-facing feature information +``` +┌─────────────────────────────────────────────────────────────┐ +│ HTTP Client │ +│ (with X-API-Key header) │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ HTTP Server (server.ts) │ +│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ ┌───────┐ │ +│ │ POST /query │ │GET /query/:id│ │GET /usage│ │/healthz│ │ +│ └──────┬──────┘ └──────┬──────┘ └────┬─────┘ └───────┘ │ +└─────────┼────────────────┼──────────────┼───────────────────┘ + │ │ │ + ▼ │ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ API Key & Quota Check │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ X-API-Key → User AI Client │ │ +│ │ No Key → Global AI Client (100/day limit) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Task Queue (in-memory) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Task { id, kind, query, status, progress, result } │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Query Router (query.ts) │ +│ ┌─────────┐ ┌──────────┐ ┌────────────────────────────┐ │ +│ │ docs │ │ source │ │ hybrid │ │ +│ └────┬────┘ └────┬─────┘ └─────────────┬──────────────┘ │ +└───────┼────────────┼──────────────────────┼─────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌───────────┐ ┌─────────────┐ ┌────────────────────────────┐ +│store.ts │ │ search.ts │ │ docs + source combined │ +│ │ │ │ │ │ +│ Gemini │ │ Local Search│ │ ┌────────┐ ┌──────────┐ │ +│ Files API │ │ + LLM │ │ │search │ │ store │ │ +└─────┬─────┘ └──────┬──────┘ │ └────┬───┘ └────┬─────┘ │ + │ │ │ │ │ │ + └──────────────┴─────────┴───────┴───────────┘ │ + │ │ + ▼ │ +┌─────────────────────────────────────────────────────────────┐ +│ Gemini API (genai.ts) │ +│ ┌──────────────────┐ ┌─────────────────────────────────┐ │ +│ │ models.generate │ │ files.upload │ │ +│ │ Content │ │ files.get │ │ +│ └──────────────────┘ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 模块职责 + +### 核心模块 + +| 模块 | 文件 | 职责 | +| ---------- | ----------- | -------------------------------------------------------- | +| **入口** | `index.ts` | 初始化流程:代理设置 → AI 客户端 → 服务器启动 → 文件上传 | +| **类型** | `types.ts` | TypeScript 类型定义 | +| **服务器** | `server.ts` | HTTP 路由、任务管理、轮询响应 | +| **查询** | `query.ts` | 查询路由、进度更新、hybrid 逻辑 | -**Source Code Queries (`query_moonbit_source`):** -- Uses files from `source/` directory -- Focuses on implementation details and API insights -- Provides information about latest features and internal code +### 数据模块 -**Flow:** -1. Receive query via MCP tool call (`query_moonbit_docs` or `query_moonbit_source`) -2. Initialize appropriate file set (documentation or source) if not already done -3. Create context-aware prompt -4. Send to Gemini Interactions API with file references -5. Return AI-generated response +| 模块 | 文件 | 职责 | +| ------------ | ----------- | --------------------------------------- | +| **文件操作** | `files.ts` | 文件读取、遍历、MIME 类型判断 | +| **缓存** | `cache.ts` | Gemini 文件缓存(`.gemini-cache.json`) | +| **Store** | `store.ts` | 文档文件上传到 Gemini | +| **搜索** | `search.ts` | 本地代码搜索 + LLM 结果组织 | -## Data Flow +### 基础模块 + +| 模块 | 文件 | 职责 | +| ------------- | ---------- | ------------------------------------- | +| **AI 客户端** | `genai.ts` | Gemini 客户端配置、代理设置、URL 重写 | +| **工具函数** | `utils.ts` | sleep、日志辅助函数 | + +## 数据流 + +### Docs 查询流程 ``` -User (Claude Desktop) - ↓ -MCP Protocol - ↓ -Tool Selection (docs or source) - ↓ -MoonverseMCPServer.handleDocQuery() or handleSourceQuery() - ↓ -Initialize files (first time only) - ↓ -Gemini Interactions API (with file context) - ↓ -AI Response - ↓ -MCP Response - ↓ -User sees answer +1. POST /query {type: "docs", query: "..."} + │ + ▼ +2. 创建 Task,状态 = "queued" + │ + ▼ +3. 异步执行 runDocsQuery() + │ + ├── 获取已上传的 store 文件 + │ + ├── 构建 Gemini 请求(文件 URI + prompt) + │ + ├── 调用 ai.models.generateContent() + │ + └── 更新 Task 状态 = "done",保存结果 + │ + ▼ +4. GET /query/:id → 返回结果 ``` -## File Structure +### Source 查询流程 ``` -moonverse.ts/ -├── src/ -│ └── index.ts # Main server implementation -├── store/ # Documentation files -│ ├── Agents.mbt.md -│ ├── ide.md -│ └── llms.txt -├── source/ # MoonBit core repository (optional) -│ └── (MoonBit source files) -├── dist/ # Compiled JavaScript -│ ├── index.js -│ └── index.d.ts -├── README.md # Project overview -├── USAGE.md # Usage guide -├── ARCHITECTURE.md # This file -├── package.json # Dependencies and scripts -├── tsconfig.json # TypeScript configuration -└── claude_desktop_config.example.json # Example config +1. POST /query {type: "source", query: ".."} + │ + ▼ +2. 创建 Task,状态 = "queued" + │ + ▼ +3. 异步执行 searchSourceCode() + │ + ├── 预处理查询(符号识别) + │ + ├── 搜索 source/core/*.mbt 文件 + │ + ├── 收集匹配行 + 上下文 + │ + ├── 调用 LLM 组织结果 + │ + └── 更新 Task 状态 = "done" + │ + ▼ +4. GET /query/:id → 返回结果 ``` -## Key Technologies +### Hybrid 查询流程 -### MCP (Model Context Protocol) -- Protocol for AI assistants to access external tools -- Uses stdio for communication -- Supports tools, resources, and prompts -- This project implements two tools for different query types +``` +1. POST /query {type: "hybrid", query: "..."} + │ + ▼ +2. 创建 Task,状态 = "queued" + │ + ▼ +3. 异步执行 runHybridQuery() + │ + ├── 搜索源代码(searchSourceCodeRaw) + │ └── 更新进度:searching-code + │ + ├── 获取 store 文件 + │ └── 更新进度:searching-docs + │ + ├── 整合代码和文档 + │ └── 更新进度:analyzing + │ + ├── 调用 Gemini(文件 + 代码 + prompt) + │ └── 更新进度:generating + │ + └── 更新 Task 状态 = "done" + │ + ▼ +4. GET /query/:id → 返回结果(含进度信息) +``` + +## 文件缓存机制 -### Gemini Interactions API -- Google's AI platform with advanced file handling -- File upload and management -- Context-aware generation with multiple files -- Supports multiple file types including source code - -### TypeScript -- Type-safe development -- ES2020 modules -- Strict mode for better error checking - -## Design Decisions - -### 1. Lazy Initialization -Files are only uploaded on the first query to avoid startup delays and API costs. Separate initialization for docs and source. - -### 2. Dual Query System -Two separate tools allow users to explicitly choose between documentation queries (user-facing) and source code queries (implementation details). - -### 3. Recursive File Discovery -The source directory is scanned recursively to handle complex repository structures with nested directories. - -### 4. In-Memory File Cache -Uploaded file metadata is stored in memory in separate lists (docs and source) to avoid re-uploading on subsequent queries within the same session. - -### 5. Extended File Type Support -Beyond just `.txt` and `.md`, the system now supports `.mbt` (MoonBit source) and `.json` files for comprehensive source code analysis. - -### 3. Error Handling -All errors are caught and returned as MCP responses with `isError: true`, ensuring the MCP client can handle failures gracefully. - -### 4. ES Modules -Uses ES modules (`type: "module"`) for compatibility with the MCP SDK and modern Node.js practices. - -### 5. Stdio Transport -Uses stdio for MCP communication, making it compatible with Claude Desktop and other MCP clients. - -## Security Considerations - -1. **API Key Protection**: API key is read from environment variables, never hardcoded -2. **File System Access**: Limited to the `store/` directory -3. **Input Validation**: All tool inputs are validated using Zod schemas -4. **Error Messages**: Generic error messages to avoid leaking sensitive information - -## Scalability - -### Current Limitations -- Files are uploaded on each server start -- No caching between sessions -- Single-threaded Node.js process - -### Future Improvements -- Persistent file store to avoid re-uploads -- Support for file search stores API (when available in SDK) -- Batch file uploads -- Caching layer for common queries -- Support for incremental document updates - -## Testing +``` +┌─────────────────────────────────────────────────────────────┐ +│ 服务器启动 │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 读取 .gemini-cache.json │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 清理过期条目(47h TTL) │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────────────────┐ +│ 缓存命中 │ │ 缓存未命中 / 哈希变化 │ +│ ↓ │ │ ↓ │ +│ 验证 Gemini 文件 │ │ 上传文件到 Gemini │ +│ ↓ │ │ ↓ │ +│ 文件存在? │ │ 更新缓存条目 │ +│ ↓ │ │ │ +│ 使用缓存 URI │ │ │ +└─────────────────────┘ └─────────────────────────────────┘ + │ │ + └───────────┬───────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 保存 .gemini-cache.json │ +└─────────────────────────────────────────────────────────────┘ +``` -Currently, the project requires manual testing with a valid Gemini API key. Future improvements could include: -- Unit tests for file processing logic -- Integration tests with mock Gemini API -- End-to-end tests with MCP client simulator +## 任务状态机 -## Monitoring +``` + ┌──────────┐ + │ queued │ + └────┬─────┘ + │ processTask() + ▼ + ┌──────────┐ + ┌────│ running │────┐ + │ └────┬─────┘ │ + │ │ │ + │ 成功 │ 失败 │ + ▼ ▼ ▼ +┌──────┐ ┌──────┐ ┌───────┐ +│ done │ │ done │ │ error │ +└──────┘ └──────┘ └───────┘ +``` -The server logs important events to stderr: -- File upload progress -- Query processing -- Errors and exceptions +### 运行中的进度阶段 -These logs can be captured by the MCP host (e.g., Claude Desktop) for debugging. +``` +initializing → searching-code → searching-docs → analyzing → generating → complete +``` + +## 轮询响应结构 + +### 运行中 + +```json +{ + "status": "running", + "phase": "generating", + "message": "正在使用 AI 分析和整理搜索结果...", + "pollCount": 2, + "elapsedSeconds": 8.5, + "codeResultsCount": 15, + "docsResultsCount": 3, + "partialContent": "正在处理 15 个匹配文件的内容...", + "nextPollSec": 4, + "etaSeconds": 5 +} +``` + +### 完成 + +```json +{ + "status": "done", + "content": "...", + "stats": { + "pollCount": 3, + "totalTimeSeconds": 12.5 + } +} +``` + +## 代理配置 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 环境变量检测 │ +│ │ +│ HTTP_PROXY / HTTPS_PROXY → 配置 undici ProxyAgent │ +│ GEMINI_BASE_URL → API 请求 URL 重写 │ +│ GEMINI_UPLOAD_BASE_URL → 文件上传 URL 重写 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 特殊查询处理 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 查询预处理 │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────────────────┐ +│ 符号查询 │ │ 普通查询 │ +│ (.. / ... / |>) │ │ (长度 > 3) │ +│ ↓ │ │ ↓ │ +│ 字面搜索 │ │ 分词 │ +│ + 语义词扩展 │ │ ↓ │ +│ │ │ 语义搜索 │ +└─────────────────────┘ └─────────────────────────────────┘ +``` + +## 依赖关系 + +``` +index.ts + ├── genai.ts (setupProxy, createAIClient, testConnection) + ├── server.ts (createServer, startServer) + └── store.ts (initializeStore) + +server.ts + ├── types.ts (Task, TaskProgress) + └── query.ts (runQuery, updateTaskProgress) + +query.ts + ├── types.ts (QueryKind, Task, TaskPhase) + ├── store.ts (getUploadedStoreFiles) + └── search.ts (searchSourceCode, searchSourceCodeRaw) + +search.ts + ├── types.ts (SearchResult) + ├── files.ts (getAllFiles, fileExists, readFileContent) + └── query.ts (updateTaskProgress) [循环依赖,通过函数参数解决] + +store.ts + ├── types.ts (FileInfo, CacheData) + ├── files.ts (getAllFiles, getMimeType, fileExists, readFileBuffer) + ├── utils.ts (sleep) + └── cache.ts (loadCache, saveCache, ...) + +cache.ts + └── types.ts (CacheData, CachedFileInfo) +``` diff --git a/Agents.md b/Agents.md new file mode 100644 index 0000000..ba74a9a --- /dev/null +++ b/Agents.md @@ -0,0 +1,374 @@ +# Moonverse.ts 开发指南 + +## 项目概述 + +MoonBit 文档搜索服务,基于 Google Gemini API,提供 HTTP JSON API。 + +- **Docs 查询**:上传 Markdown/txt 文件到 Gemini,使用 AI 回答文档问题 +- **Source 查询**:本地搜索源代码 + LLM 组织结果,生成高质量文档 +- **Hybrid 查询**:结合文档和代码示例,提供综合回答 + +## 常用命令 + +### 开发环境 + +```bash +# 安装依赖 +yarn install + +# 构建项目 +yarn build + +# 开发模式运行(热重载) +yarn dev + +# 生产模式运行 +yarn start +``` + +### 环境变量 + +```bash +# 必需:Gemini API Key +export GEMINI_API_KEY=your-api-key-here + +# 可选:代理配置 +export HTTP_PROXY=http://localhost:7890 +export HTTPS_PROXY=http://localhost:7890 + +# 可选:自定义 Gemini 端点 +export GEMINI_BASE_URL=https://your-proxy.example.com +export GEMINI_UPLOAD_BASE_URL=https://your-proxy.example.com + +# 可选:服务器端口(默认 8080) +export PORT=8080 +``` + +### 测试命令 + +```bash +# 测试文档查询(带自己的 API Key,推荐) +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-gemini-api-key" \ + -d '{"type": "docs", "query": "MoonBit 如何声明可变变量?"}' + +# 测试源代码查询(使用全局 Key,每天限 100 次) +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "source", "query": "Array"}' + +# 测试混合查询(推荐) +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-gemini-api-key" \ + -d '{"type": "hybrid", "query": "MoonBit 当中 .. 两个点有哪些用法?"}' + +# 轮询任务状态 +curl http://localhost:8080/query/ + +# 健康检查 +curl http://localhost:8080/healthz + +# 查看全局 API 使用情况 +curl http://localhost:8080/usage +``` + +### 服务器管理 + +```bash +# 启动服务器(前台) +npx tsx src/index.ts + +# 启动服务器(后台 + 日志) +npx tsx src/index.ts > /tmp/moonverse.log 2>&1 & + +# 查看日志 +tail -f /tmp/moonverse.log + +# 停止服务器 +pkill -f "tsx src/index" + +# 查看端口占用 +lsof -i :8080 +``` + +### 目录结构 + +``` +moonverse.ts/ +├── src/ +│ ├── index.ts # 主入口 +│ ├── types.ts # 类型定义 +│ ├── utils.ts # 工具函数 +│ ├── files.ts # 文件系统操作 +│ ├── genai.ts # AI 客户端配置 +│ ├── cache.ts # 文件缓存管理 +│ ├── store.ts # Store 文件上传 +│ ├── search.ts # 源码搜索 + LLM +│ ├── query.ts # 查询执行逻辑 +│ └── server.ts # HTTP 服务器 +├── store/ # Markdown 文档(上传到 Gemini) +│ ├── Agents.mbt.md +│ ├── ide.md +│ └── llms.txt +├── source/ # MoonBit 源代码(本地搜索) +│ └── core/ # MoonBit core 库源码 +├── tests/ # 测试文件(已 gitignore) +├── .gemini-cache.json # Gemini 文件缓存(已 gitignore) +├── package.json +└── tsconfig.json +``` + +## API 接口 + +### POST /query + +创建查询任务(异步) + +**请求头:** + +| Header | 必需 | 说明 | +| ------------- | ---- | -------------------------------------------- | +| Content-Type | 是 | `application/json` | +| X-API-Key | 否 | 用户自己的 Gemini API Key(推荐携带) | + +**请求体:** + +```json +{ + "type": "docs" | "source" | "hybrid", + "query": "你的问题" +} +``` + +| 类型 | 说明 | +| -------- | -------------------------------- | +| `docs` | 仅使用上传的文档回答 | +| `source` | 本地搜索源代码 + LLM 组织 | +| `hybrid` | 结合文档和代码,综合回答(推荐) | + +**响应:** + +```json +{ + "id": "uuid", + "nextPollSec": 3, + "globalUsage": { // 仅当使用全局 Key 时返回 + "date": "2025-12-18", + "used": 5, + "remaining": 95 + } +} +``` + +**429 错误(全局配额已用完):** + +```json +{ + "error": "全局 API 配额已用完,请在请求头中提供 X-API-Key", + "hint": "请在 Header 中添加: X-API-Key: your-gemini-api-key", + "usage": { "date": "2025-12-18", "used": 100, "remaining": 0 } +} +``` + +### GET /query/:id + +轮询任务状态 + +**响应(运行中):** + +```json +{ + "status": "running", + "phase": "searching-code", + "message": "正在搜索 MoonBit 核心库... (已找到 5 个 SDK 示例)", + "pollCount": 1, + "elapsedSeconds": 5.2, + "codeResultsCount": 15, + "docsResultsCount": 3, + "partialContent": "匹配文件: array/array.mbt, buffer/buffer.mbt 等", + "nextPollSec": 4, + "etaSeconds": 10 +} +``` + +**任务阶段(phase):** + +| 阶段 | 说明 | +| ---------------- | -------------- | +| `initializing` | 任务初始化中 | +| `searching-code` | 正在搜索源代码 | +| `searching-docs` | 正在准备文档 | +| `analyzing` | 正在分析和整合 | +| `generating` | 正在生成回答 | +| `complete` | 完成 | + +**响应(完成):** + +```json +{ + "status": "done", + "content": "AI 生成的中文回答...", + "stats": { + "pollCount": 3, + "totalTimeSeconds": 12.5 + } +} +``` + +**响应(错误):** + +```json +{ + "status": "error", + "error": "错误信息", + "stats": { + "pollCount": 2, + "totalTimeSeconds": 5.0 + } +} +``` + +### GET /healthz + +健康检查 + +```json +{ + "ok": true +} +``` + +## 技术栈 + +- **运行时**: Node.js 20+ +- **语言**: TypeScript (ES Modules) +- **AI SDK**: `@google/genai` v1.34.0 +- **HTTP 代理**: `undici` ProxyAgent(可选) +- **验证**: `zod` + +## 工作流程 + +### Docs 查询流程 + +1. 服务器启动时,检查本地缓存 `.gemini-cache.json` +2. 对比文件哈希,仅上传新增/修改的文件到 Gemini +3. 收到 docs 查询请求,创建异步任务 +4. 使用上传的文件 URI 和用户问题调用 Gemini +5. 返回中文 AI 回答 + +### Source 查询流程 + +1. 收到 source 查询请求,创建异步任务 +2. 预处理查询(支持符号搜索如 `..`) +3. 在 `source/core/` 目录递归搜索 `.mbt` 文件 +4. 收集匹配的代码行和上下文 +5. 使用 LLM 组织和解释搜索结果 +6. 返回中文格式化回答 + +### Hybrid 查询流程(推荐) + +1. 收到 hybrid 查询请求,创建异步任务 +2. 并行搜索源代码和准备文档 +3. 将代码示例和文档一起发送给 Gemini +4. AI 综合两种来源生成全面回答 +5. 返回带代码示例的中文回答 + +## 文件缓存机制 + +服务器使用 `.gemini-cache.json` 缓存已上传的文件: + +```json +{ + "version": 1, + "files": [ + { + "uri": "https://generativelanguage.googleapis.com/v1beta/files/xxx", + "mimeType": "text/markdown", + "name": "files/xxx", + "displayName": "Agents.mbt.md", + "fileHash": "md5-hash", + "uploadedAt": 1702900000000, + "expiresAt": 1703069200000 + } + ] +} +``` + +**特性:** + +- 文件哈希校验:内容变化时自动重新上传 +- 47 小时有效期(Gemini 文件 48 小时过期) +- 启动时验证文件是否仍在 Gemini 存在 +- 自动清理过期缓存条目 + +## 特殊搜索支持 + +支持搜索特殊符号和短查询: + +| 查询 | 搜索策略 | +| -------- | ------------------------------------------- | +| `..` | 字面搜索 + 语义词(range, cascade, update) | +| `...` | 字面搜索 + 语义词(spread, rest) | +| `\|>` | 字面搜索 + 语义词(pipe, pipeline) | +| 普通查询 | 分词后语义搜索 | + +## 注意事项 + +1. **代理配置**:通过环境变量 `HTTP_PROXY`/`HTTPS_PROXY` 配置(可选) +2. **文件上传**:只上传 `.md` 和 `.txt` 文件,源代码不上传 +3. **异步处理**:查询采用任务队列模式,客户端需要轮询结果 +4. **中文回答**:所有 AI 回答默认使用中文 +5. **缓存文件**:`.gemini-cache.json` 已加入 `.gitignore` + +## 故障排查 + +### 文件上传失败 + +```bash +# 检查代理是否运行(如果配置了代理) +curl -x http://localhost:7890 https://www.google.com + +# 检查 API Key +echo $GEMINI_API_KEY +``` + +### 缓存问题 + +```bash +# 删除缓存,强制重新上传 +rm .gemini-cache.json + +# 重启服务器 +pkill -f "tsx src/index" && npx tsx src/index.ts +``` + +### 服务器无响应 + +```bash +# 检查进程 +ps aux | grep tsx + +# 检查端口 +lsof -i :8080 + +# 重启服务器 +pkill -f "tsx src/index" +npx tsx src/index.ts > /tmp/moonverse.log 2>&1 & +``` + +## 模块说明 + +| 模块 | 职责 | +| ----------- | ---------------------------------------- | +| `types.ts` | 类型定义(Task, FileInfo, CacheData 等) | +| `utils.ts` | 工具函数(sleep, log) | +| `files.ts` | 文件系统操作(读取、遍历、MIME 类型) | +| `genai.ts` | AI 客户端配置和代理设置 | +| `cache.ts` | Gemini 文件缓存管理 | +| `store.ts` | Store 文件上传到 Gemini | +| `search.ts` | 本地代码搜索 + LLM 组织 | +| `query.ts` | 查询执行和进度更新 | +| `server.ts` | HTTP 服务器和路由 | +| `index.ts` | 主入口,初始化流程 | diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..27a7526 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# Build stage +FROM node:20-slim AS builder + +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock ./ + +# Install dependencies +RUN yarn install --frozen-lockfile + +# Copy source code +COPY tsconfig.json ./ +COPY src/ ./src/ + +# Build TypeScript +RUN yarn build + +# Production stage +FROM node:20-slim AS runner + +WORKDIR /app + +# Copy package files and install production dependencies only +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile --production && yarn cache clean + +# Copy built files +COPY --from=builder /app/dist ./dist + +# Copy store and source directories +COPY store/ ./store/ +COPY source/ ./source/ + +# Install necessary tools for MoonBit setup +RUN apt-get update && apt-get install -y \ + git \ + curl \ + wget \ + tree \ + && rm -rf /var/lib/apt/lists/* + +# Install MoonBit related dependencies +RUN cd source && git clone https://github.com/moonbitlang/core.git --depth=1 moonbit-core +RUN cd source && git clone https://github.com/moonbitlang/async.git --depth=1 moonbit-async +RUN curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash + +# Environment variables (can be overridden at runtime) +ENV PORT=8080 +ENV NODE_ENV=production + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthz || exit 1 + +# Run the server +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index 7066a72..dcabf76 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,120 @@ # moonverse.ts -MoonBit documentation and source code search powered by Gemini's Interactions API and MCP (Model Context Protocol). +MoonBit 文档和源代码搜索服务,基于 Google Gemini API。 -## Overview +## 功能特性 -This is a TypeScript command-line tool that implements an MCP Server. It reads text files from the `store/` directory (documentation) and `source/` directory (MoonBit core repository), uses Gemini's Interactions API to provide AI-powered search and question answering. +- 🔍 **三种查询模式**:文档查询、源码查询、混合查询 +- 📚 **文档搜索**:上传 Markdown 文件到 Gemini,AI 回答问题 +- 💻 **源码搜索**:本地搜索 MoonBit 源代码 + LLM 组织结果 +- 🔗 **混合查询**:结合文档和代码示例,提供综合回答 +- 💾 **文件缓存**:自动缓存已上传文件,避免重复上传 +- 🌐 **HTTP API**:异步任务队列,支持轮询获取进度 +- 🇨🇳 **中文回答**:所有 AI 回答默认使用中文 -## Features +## 快速开始 -- 🚀 MCP Server implementation for integration with Claude Desktop and other MCP clients -- 📚 Automatic document ingestion from `store/` directory -- 💻 Source code analysis from `source/` directory (MoonBit core repository) -- 🔍 AI-powered semantic search using Gemini's Interactions API -- 💬 Dual query modes: documentation and source code -- 📝 Support for text, markdown, JSON, and MoonBit source files +### 1. 安装依赖 -## Prerequisites - -- Node.js 18.0.0 or higher -- A Google AI (Gemini) API key -- MoonBit core repository (optional, for source code queries) - -## Installation - -1. Clone the repository: -```bash -git clone -cd moonverse.ts -``` - -2. Install dependencies: ```bash yarn install ``` -3. Build the project: -```bash -yarn build -``` - -## Configuration - -Set your Gemini API key as an environment variable: +### 2. 配置环境变量 ```bash export GEMINI_API_KEY='your-api-key-here' ``` -You can get a Gemini API key from [Google AI Studio](https://makersuite.google.com/app/apikey). - -## Usage +### 3. 构建并运行 -### As an MCP Server - -Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): - -```json -{ - "mcpServers": { - "moonverse": { - "command": "node", - "args": ["/path/to/moonverse.ts/dist/index.js"], - "env": { - "GEMINI_API_KEY": "your-api-key-here" - } - } - } -} +```bash +yarn build +yarn start ``` -### Running Directly +### 4. 测试查询 ```bash -# Development mode with tsx -yarn dev +# 混合查询(推荐) +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "hybrid", "query": "MoonBit 当中 .. 两个点有哪些用法?"}' -# Production mode -yarn start +# 轮询结果 +curl http://localhost:8080/query/ ``` -## Document Store - -Place your documentation files in the `store/` directory. Supported file types: -- `.txt` - Plain text files -- `.md` - Markdown files - -The project includes sample MoonBit documentation: -- `llms.txt` - General MoonBit language information -- `Agents.mbt.md` - MoonBit agent documentation -- `ide.md` - IDE integration information +## API 接口 -## Source Code Repository +| 端点 | 方法 | 说明 | +| ------------ | ---- | ------------------ | +| `/query` | POST | 创建查询任务 | +| `/query/:id` | GET | 轮询任务状态 | +| `/healthz` | GET | 健康检查 | +| `/usage` | GET | 全局 API 使用情况 | -Place the MoonBit core repository in the `source/` directory for source code queries. Supported file types: -- `.mbt` - MoonBit source files -- `.md` - Markdown documentation -- `.json` - JSON configuration files -- `.txt` - Plain text files +### API Key 认证 -This enables querying about: -- Latest API features -- Implementation details -- Pre-release functionality -- Internal code structure +推荐在请求头中携带自己的 Gemini API Key: -## MCP Tools +```bash +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-gemini-api-key" \ + -d '{"type": "hybrid", "query": "..."}' +``` -The server provides the following tools: +不携带 `X-API-Key` 时,将使用全局 Key(每天限 100 次)。 -### `query_moonbit_docs` +### 查询类型 -Query the MoonBit documentation using AI-powered semantic search. +| 类型 | 说明 | 适用场景 | +| -------- | ---------- | -------------------------- | +| `docs` | 仅文档查询 | 查询官方文档内容 | +| `source` | 源码搜索 | 查找代码示例和实现 | +| `hybrid` | 混合查询 | 综合文档和代码回答(推荐) | -**Parameters:** -- `query` (string): The question to ask about the MoonBit documentation +## 项目结构 -**Example:** ``` -Use the query_moonbit_docs tool to ask: "What are the key features of MoonBit?" +moonverse.ts/ +├── src/ # 源代码 +│ ├── index.ts # 主入口 +│ ├── types.ts # 类型定义 +│ ├── genai.ts # AI 客户端 +│ ├── cache.ts # 文件缓存 +│ ├── store.ts # 文件上传 +│ ├── search.ts # 源码搜索 +│ ├── query.ts # 查询执行 +│ └── server.ts # HTTP 服务器 +├── store/ # 文档目录(上传到 Gemini) +├── source/ # 源代码目录(本地搜索) +└── .gemini-cache.json # 文件缓存 ``` -### `query_moonbit_source` - -Query the MoonBit core source code repository to understand latest API features and implementations. - -**Parameters:** -- `query` (string): The question to ask about MoonBit source code or latest features +## 环境变量 -**Example:** -``` -Use the query_moonbit_source tool to ask: "How is the type inference system implemented?" -``` +| 变量 | 必需 | 说明 | +| ----------------- | ---- | ----------------------- | +| `GEMINI_API_KEY` | ✅ | Gemini API 密钥 | +| `HTTP_PROXY` | ❌ | HTTP 代理地址 | +| `HTTPS_PROXY` | ❌ | HTTPS 代理地址 | +| `GEMINI_BASE_URL` | ❌ | 自定义 Gemini API 端点 | +| `PORT` | ❌ | 服务器端口(默认 8080) | -## API References +## 技术栈 -This project uses: -- [Gemini Interactions API](https://ai.google.dev/gemini-api/docs/interactions) -- [Google AI JavaScript SDK](https://github.com/googleapis/js-genai) -- [Model Context Protocol SDK](https://github.com/modelcontextprotocol/sdk) +- **运行时**: Node.js 20+ +- **语言**: TypeScript (ES Modules) +- **AI SDK**: `@google/genai` v1.34.0 +- **验证**: `zod` -## Development - -```bash -# Build the project -yarn build +## 文档 -# Run in development mode -yarn dev -``` +- [开发指南](Agents.md) - 详细的开发文档和 API 说明 +- [架构设计](ARCHITECTURE.md) - 系统架构和模块说明 ## License ISC - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/USAGE.md b/USAGE.md index df1eb60..be026cf 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,120 +1,260 @@ -# Usage Guide +# 使用指南 -## Quick Start +## 快速开始 + +### 1. 设置 API Key -1. **Set up your API key**: ```bash export GEMINI_API_KEY='your-api-key-here' ``` -2. **Build the project**: +可从 [Google AI Studio](https://makersuite.google.com/app/apikey) 获取 API Key。 + +### 2. 构建项目 + ```bash yarn install yarn build ``` -3. **Configure Claude Desktop**: +### 3. 启动服务器 -Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the equivalent on your OS: - -```json -{ - "mcpServers": { - "moonverse": { - "command": "node", - "args": ["/absolute/path/to/moonverse.ts/dist/index.js"], - "env": { - "GEMINI_API_KEY": "your-api-key-here" - } - } - } -} +```bash +yarn start ``` -4. **Restart Claude Desktop** +服务器默认运行在 `http://localhost:8080`。 -## Using the Tool +## 查询示例 -Once configured, you can use both tools in Claude Desktop: +### API Key 认证 -### Documentation Queries +**推荐方式**:在请求头中携带自己的 Gemini API Key -**Basic questions:** -``` -Use query_moonbit_docs to ask: "What is MoonBit?" +```bash +# 带自己的 API Key(无使用限制) +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-gemini-api-key" \ + -d '{"type": "hybrid", "query": "..."}' ``` -**Technical questions:** +**备用方式**:使用服务器全局 Key(每天限 100 次) + +```bash +# 不带 API Key,使用全局配额 +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "hybrid", "query": "..."}' + +# 响应中会包含使用情况 +# {"id": "xxx", "nextPollSec": 3, "globalUsage": {"used": 5, "remaining": 95}} + +# 查看全局使用情况 +curl http://localhost:8080/usage ``` -Use query_moonbit_docs to ask: "How do I use the moon ide command for code navigation?" + +### 混合查询(推荐) + +结合文档和代码示例,提供最全面的回答: + +```bash +# 查询语法 +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "hybrid", "query": "MoonBit 当中 .. 两个点有哪些用法?"}' + +# 查询数据结构 +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "hybrid", "query": "如何在 MoonBit 中使用 Array?"}' + +# 查询特定功能 +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "hybrid", "query": "MoonBit 的模式匹配怎么用?"}' ``` -**Feature inquiries:** +### 文档查询 + +仅基于上传的文档回答: + +```bash +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "docs", "query": "MoonBit 如何声明可变变量?"}' ``` -Use query_moonbit_docs to ask: "What are the key features of MoonBit's type system?" + +### 源码查询 + +搜索 MoonBit 源代码并用 AI 组织结果: + +```bash +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "source", "query": "Buffer"}' ``` -### Source Code Queries +### 轮询结果 -**API questions:** +查询是异步的,需要轮询获取结果: + +```bash +# 创建查询 +RESPONSE=$(curl -s -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "hybrid", "query": "什么是 MoonBit?"}') + +# 提取任务 ID +TASK_ID=$(echo $RESPONSE | jq -r '.id') +echo "Task ID: $TASK_ID" + +# 轮询结果 +while true; do + RESULT=$(curl -s "http://localhost:8080/query/$TASK_ID") + STATUS=$(echo $RESULT | jq -r '.status') + + if [ "$STATUS" = "done" ]; then + echo $RESULT | jq -r '.content' + break + elif [ "$STATUS" = "error" ]; then + echo "Error: $(echo $RESULT | jq -r '.error')" + break + else + echo "Status: $STATUS - $(echo $RESULT | jq -r '.message')" + sleep $(echo $RESULT | jq -r '.nextPollSec') + fi +done ``` -Use query_moonbit_source to ask: "What are the latest API additions in the core library?" + +## 添加文档 + +### 添加新文档 + +将 `.md` 或 `.txt` 文件放入 `store/` 目录: + +```bash +# 添加新文档 +cp my-doc.md store/ + +# 重启服务器 +pkill -f "tsx src/index" && yarn start ``` -**Implementation questions:** +### 添加源代码 + +将 MoonBit 源代码放入 `source/` 目录: + +```bash +# 克隆 MoonBit core +git clone https://github.com/moonbitlang/core.git source/core ``` -Use query_moonbit_source to ask: "How is pattern matching implemented?" + +## 环境变量 + +| 变量 | 说明 | 示例 | +| ----------------- | ----------------------- | --------------------------- | +| `GEMINI_API_KEY` | Gemini API 密钥(必需) | `AIza...` | +| `HTTP_PROXY` | HTTP 代理地址 | `http://localhost:7890` | +| `HTTPS_PROXY` | HTTPS 代理地址 | `http://localhost:7890` | +| `GEMINI_BASE_URL` | 自定义 API 端点 | `https://proxy.example.com` | +| `PORT` | 服务器端口 | `8080` | + +## 响应格式 + +### 运行中 + +```json +{ + "status": "running", + "phase": "generating", + "message": "正在使用 AI 分析和整理搜索结果...", + "pollCount": 1, + "elapsedSeconds": 5.2, + "codeResultsCount": 15, + "partialContent": "匹配文件: array/array.mbt, buffer/buffer.mbt 等", + "nextPollSec": 4 +} ``` -**Pre-release features:** +### 完成 + +```json +{ + "status": "done", + "content": "在 MoonBit 中,`..` 运算符有以下几种用法...", + "stats": { + "pollCount": 3, + "totalTimeSeconds": 12.5 + } +} ``` -Use query_moonbit_source to ask: "What new features are in development?" + +### 错误 + +```json +{ + "status": "error", + "error": "API quota exceeded", + "stats": { + "pollCount": 1, + "totalTimeSeconds": 2.0 + } +} ``` -## Adding More Documents +## 特殊查询 + +支持搜索特殊符号: -To add more documentation to the search: +| 查询 | 说明 | +| ----- | ---------------------------- | +| `..` | 范围操作符、记录更新、方法链 | +| `...` | 展开操作符 | +| `\|>` | 管道操作符 | +| `::` | 方法访问 | -1. Place `.txt` or `.md` files in the `store/` directory for documentation -2. Place MoonBit source files (`.mbt`, `.md`, `.json`) in the `source/` directory for source code queries -3. Restart the MCP server (restart Claude Desktop) -4. The new files will be automatically uploaded and included in searches +示例: -## Supported File Types +```bash +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{"type": "hybrid", "query": ".."}' +``` -**Documentation (store/ directory):** -- `.txt` - Plain text files -- `.md` - Markdown files +## 故障排查 -**Source Code (source/ directory):** -- `.mbt` - MoonBit source files -- `.md` - Markdown documentation -- `.json` - JSON configuration -- `.txt` - Plain text files +### 服务器无法启动 -## How It Works +```bash +# 检查端口占用 +lsof -i :8080 -1. On startup, the server prepares to read from both `store/` (documentation) and `source/` (code) directories -2. When you query docs, files from `store/` are uploaded to Gemini's Interactions API -3. When you query source, files from `source/` are uploaded to Gemini's Interactions API -4. Files are uploaded lazily on first query and cached in memory -5. The AI provides context-aware answers based on the uploaded files +# 杀死占用进程 +lsof -ti :8080 | xargs kill -9 +``` -## Troubleshooting +### API 调用失败 -### "GEMINI_API_KEY environment variable is not set" -Make sure you've set the `GEMINI_API_KEY` in your environment or in the Claude Desktop config. +```bash +# 检查 API Key +echo $GEMINI_API_KEY -### "No text or markdown files found in store directory" -Ensure you have `.txt` or `.md` files in the `store/` directory. +# 测试 API 连接 +curl "https://generativelanguage.googleapis.com/v1beta/models?key=$GEMINI_API_KEY" +``` -### Files not updating -Restart Claude Desktop to reload the MCP server and re-upload files. +### 文件上传失败 -## API Rate Limits +```bash +# 删除缓存重试 +rm .gemini-cache.json +yarn start +``` -Be aware of Gemini API rate limits. The free tier has: -- 60 requests per minute -- 1,500 requests per day +### 代理问题 -For production use, consider the paid tier for higher limits. +```bash +# 测试代理 +curl -x http://localhost:7890 https://www.google.com +``` diff --git a/claude_desktop_config.example.json b/claude_desktop_config.example.json deleted file mode 100644 index e7b03d0..0000000 --- a/claude_desktop_config.example.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "mcpServers": { - "moonverse": { - "command": "node", - "args": [ - "/absolute/path/to/moonverse.ts/dist/index.js" - ], - "env": { - "GEMINI_API_KEY": "your-gemini-api-key-here" - } - } - } -} diff --git a/examples/test_client.mts b/examples/test_client.mts new file mode 100644 index 0000000..2a2d7b5 --- /dev/null +++ b/examples/test_client.mts @@ -0,0 +1,70 @@ +const BASE_URL = "http://localhost:8080"; + +async function pollTask(id: string) { + console.log(`Polling task ${id}...`); + while (true) { + const res = await fetch(`${BASE_URL}/query/${id}`); + if (!res.ok) { + throw new Error(`Failed to poll task: ${res.status} ${res.statusText}`); + } + const data = (await res.json()) as any; + + if (data.status === "done") { + console.log(`Task ${id} completed!`); + return data.content; + } else if (data.status === "error") { + throw new Error(`Task failed: ${data.error}`); + } else { + console.log( + `Task ${id} status: ${data.status}. Next poll in ${data.nextPollSec}s...` + ); + await new Promise((resolve) => + setTimeout(resolve, (data.nextPollSec || 2) * 1000) + ); + } + } +} + +async function createQuery(type: "docs" | "source", query: string) { + console.log(`Creating ${type} query: "${query}"`); + const res = await fetch(`${BASE_URL}/query`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type, query }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to create query: ${res.status} ${text}`); + } + + const data = (await res.json()) as any; + console.log(`Query created, task ID: ${data.id}`); + return data.id; +} + +async function main() { + try { + // Test 1: Docs Query + console.log("\n--- Test 1: Docs Query ---"); + const docsId = await createQuery( + "docs", + "How to declare a mutable variable in MoonBit?" + ); + const docsResult = await pollTask(docsId); + console.log("\nResult:\n", docsResult); + + // Test 2: Source Query + console.log("\n--- Test 2: Source Query ---"); + const sourceId = await createQuery( + "source", + "How is the Array sort implemented?" + ); + const sourceResult = await pollTask(sourceId); + console.log("\nResult:\n", sourceResult); + } catch (error) { + console.error("Test failed:", error); + } +} + +main(); diff --git a/examples/test_client_simple.mts b/examples/test_client_simple.mts new file mode 100644 index 0000000..92a8a84 --- /dev/null +++ b/examples/test_client_simple.mts @@ -0,0 +1,73 @@ +// Simple test client without proxy configuration +const BASE_URL = "http://localhost:8080"; + +async function pollTask(id: string) { + console.log(`Polling task ${id}...`); + while (true) { + const res = await fetch(`${BASE_URL}/query/${id}`); + if (!res.ok) { + throw new Error(`Failed to poll task: ${res.status} ${res.statusText}`); + } + const data = (await res.json()) as any; + + if (data.status === "done") { + console.log(`Task ${id} completed!`); + return data.content; + } else if (data.status === "error") { + throw new Error(`Task failed: ${data.error}`); + } else { + console.log( + `Task ${id} status: ${data.status}. Next poll in ${data.nextPollSec}s...` + ); + await new Promise((resolve) => + setTimeout(resolve, (data.nextPollSec || 2) * 1000) + ); + } + } +} + +async function createQuery(type: "docs" | "source", query: string) { + console.log(`Creating ${type} query: "${query}"`); + const res = await fetch(`${BASE_URL}/query`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type, query }), + }); + + if (!res.ok) { + throw new Error(`Failed to create query: ${res.status} ${res.statusText}`); + } + + const data = (await res.json()) as any; + console.log(`Query created with ID: ${data.id}`); + return data.id; +} + +async function main() { + try { + console.log("\n--- Test 1: Docs Query ---"); + const docsQueryId = await createQuery( + "docs", + "How to declare a mutable variable in MoonBit?" + ); + const docsResult = await pollTask(docsQueryId); + console.log("Docs query result:"); + console.log(docsResult); + + console.log("\n--- Test 2: Source Query ---"); + const sourceQueryId = await createQuery( + "source", + "Show me the implementation of array sorting" + ); + const sourceResult = await pollTask(sourceQueryId); + console.log("Source query result:"); + console.log(sourceResult); + + console.log("\n✅ All tests passed!"); + } catch (error) { + console.error("Test failed:", error); + process.exit(1); + } +} + +main(); diff --git a/examples/test_file_upload.mts b/examples/test_file_upload.mts new file mode 100644 index 0000000..3c7299c --- /dev/null +++ b/examples/test_file_upload.mts @@ -0,0 +1,47 @@ +import { GoogleGenAI } from "@google/genai"; +import { ProxyAgent, setGlobalDispatcher } from "undici"; + +// Configure proxy using undici +const proxyUrl = + process.env.HTTP_PROXY || process.env.HTTPS_PROXY || "http://localhost:7890"; +const proxyAgent = new ProxyAgent(proxyUrl); +setGlobalDispatcher(proxyAgent); + +console.log(`Using proxy: ${proxyUrl}`); + +const apiKey = process.env.GEMINI_API_KEY || ""; +const ai = new GoogleGenAI({ + apiKey, + httpOptions: { + baseUrl: process.env.GEMINI_BASE_URL, + }, +}); + +async function test() { + console.log("Testing file upload through proxy..."); + + try { + const testContent = "This is a test file."; + const blob = new Blob([testContent], { type: "text/plain" }); + + const uploadedFile = await ai.files.upload({ + file: blob, + config: { + mimeType: "text/plain", + displayName: "test.txt", + }, + }); + + console.log("Upload successful!"); + console.log("File name:", uploadedFile.name); + console.log("File URI:", uploadedFile.uri); + + // Get file status + const fileInfo = await ai.files.get({ name: uploadedFile.name as string }); + console.log("File state:", fileInfo.state); + } catch (error) { + console.error("Upload failed:", error); + } +} + +test(); diff --git a/examples/test_part.mts b/examples/test_part.mts new file mode 100644 index 0000000..2d5a824 --- /dev/null +++ b/examples/test_part.mts @@ -0,0 +1,8 @@ +import { createPartFromUri } from "@google/genai"; + +const part = createPartFromUri( + "https://generativelanguage.googleapis.com/v1beta/files/test123", + "text/plain" +); + +console.log("Part structure:", JSON.stringify(part, null, 2)); diff --git a/k8s.yaml b/k8s.yaml new file mode 100644 index 0000000..488ddbe --- /dev/null +++ b/k8s.yaml @@ -0,0 +1,124 @@ +# Moonverse K8s Deployment +# Usage: +# kubectl apply -f k8s-secrets.yaml # Apply secrets first (not in git) +# kubectl apply -f k8s.yaml # Apply main deployment + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: moonverse + labels: + app: moonverse + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: moonverse-config + namespace: moonverse +data: + PORT: "8080" + NODE_ENV: "production" + # Optional: Custom Gemini endpoint + GEMINI_BASE_URL: "https://ja.chenyong.life" + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: moonverse + namespace: moonverse + labels: + app: moonverse +spec: + replicas: 1 + selector: + matchLabels: + app: moonverse + template: + metadata: + labels: + app: moonverse + spec: + imagePullSecrets: + - name: ghcr-login-secret + containers: + - name: moonverse + # TODO: Replace with your actual image registry + image: ghcr.io/termina/moonverse.ts:pr-0ed76c2 + imagePullPolicy: Always + ports: + - containerPort: 8080 + protocol: TCP + envFrom: + - configMapRef: + name: moonverse-config + - secretRef: + name: moonverse-secrets + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + +--- +apiVersion: v1 +kind: Service +metadata: + name: moonverse + namespace: moonverse + labels: + app: moonverse +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: moonverse + +--- +# Ingress for external access with HTTPS (using Traefik + cert-manager) +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: moonverse + namespace: moonverse + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + rules: + - host: moonverse-api.tiye.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: moonverse + port: + number: 80 + tls: + - hosts: + - moonverse-api.tiye.me + secretName: moonverse-tls-cert diff --git a/package.json b/package.json index 13955f6..6ea71d4 100644 --- a/package.json +++ b/package.json @@ -12,18 +12,27 @@ "start": "node dist/index.js", "dev": "tsx src/index.ts", "prepublishOnly": "yarn build", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "format": "prettier --write \"src/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\"" }, - "keywords": ["mcp", "moonbit", "gemini", "ai", "documentation"], + "keywords": [ + "moonbit", + "gemini", + "ai", + "documentation" + ], "author": "", "license": "ISC", "dependencies": { - "@google/generative-ai": "^0.24.1", - "@modelcontextprotocol/sdk": "^1.21.1", + "@google/genai": "^1.34.0", + "https-proxy-agent": "^7.0.6", + "undici": "^7.16.0", "zod": "^3.25.76" }, "devDependencies": { "@types/node": "^24.10.1", + "prettier": "^3.7.4", "tsx": "^4.20.6", "typescript": "^5.9.3" } diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..e6a86d4 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,113 @@ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import { CacheData, CachedFileInfo } from './types.js'; + +// ---------- Constants ---------- + +const CACHE_VERSION = 1; +const CACHE_FILENAME = '.gemini-cache.json'; + +// Gemini Files API: files expire after 48 hours +// Use 47 hours to be safe +const FILE_TTL_MS = 47 * 60 * 60 * 1000; + +// ---------- Cache Operations ---------- + +function getCachePath(rootDir: string): string { + return path.join(rootDir, CACHE_FILENAME); +} + +export function loadCache(rootDir: string): CacheData { + const cachePath = getCachePath(rootDir); + try { + if (fs.existsSync(cachePath)) { + const raw = fs.readFileSync(cachePath, 'utf-8'); + const data = JSON.parse(raw) as CacheData; + if (data.version === CACHE_VERSION) { + return data; + } + console.log(`[Cache] Version mismatch, creating new cache`); + } + } catch (error) { + console.log(`[Cache] Failed to load cache, creating new one:`, error); + } + return { version: CACHE_VERSION, files: [] }; +} + +export function saveCache(rootDir: string, cache: CacheData): void { + const cachePath = getCachePath(rootDir); + try { + fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8'); + console.log(`[Cache] Saved ${cache.files.length} entries to ${CACHE_FILENAME}`); + } catch (error) { + console.error(`[Cache] Failed to save cache:`, error); + } +} + +// ---------- File Hash ---------- + +export function computeFileHash(filePath: string): string { + const content = fs.readFileSync(filePath); + return crypto.createHash('md5').update(content).digest('hex'); +} + +// ---------- Cache Lookup ---------- + +export function findValidCachedFile( + cache: CacheData, + displayName: string, + fileHash: string +): CachedFileInfo | null { + const now = Date.now(); + const entry = cache.files.find((f) => f.displayName === displayName && f.fileHash === fileHash); + + if (entry && entry.expiresAt > now) { + return entry; + } + return null; +} + +// ---------- Cache Update ---------- + +export function addOrUpdateCacheEntry( + cache: CacheData, + entry: Omit +): CachedFileInfo { + const now = Date.now(); + const newEntry: CachedFileInfo = { + ...entry, + uploadedAt: now, + expiresAt: now + FILE_TTL_MS, + }; + + // Remove old entry with same displayName if exists + cache.files = cache.files.filter((f) => f.displayName !== entry.displayName); + cache.files.push(newEntry); + + return newEntry; +} + +// ---------- Cache Cleanup ---------- + +export function cleanExpiredEntries(cache: CacheData): number { + const now = Date.now(); + const before = cache.files.length; + cache.files = cache.files.filter((f) => f.expiresAt > now); + return before - cache.files.length; +} + +// ---------- Time Helpers ---------- + +export function formatTimeRemaining(expiresAt: number): string { + const remaining = expiresAt - Date.now(); + if (remaining <= 0) return 'expired'; + + const hours = Math.floor(remaining / (60 * 60 * 1000)); + const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000)); + + if (hours > 0) { + return `${hours}h ${minutes}m remaining`; + } + return `${minutes}m remaining`; +} diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..735b46a --- /dev/null +++ b/src/files.ts @@ -0,0 +1,47 @@ +import fs from 'fs'; +import path from 'path'; + +// ---------- File System Helpers ---------- + +export function getMimeType(ext: string): string { + switch (ext.toLowerCase()) { + case '.md': + return 'text/markdown'; + case '.txt': + return 'text/plain'; + case '.json': + return 'application/json'; + case '.mbt': + return 'text/plain'; + case '.ts': + return 'text/typescript'; + default: + return 'text/plain'; + } +} + +export function getAllFiles(dirPath: string, acc: string[] = []): string[] { + const files = fs.readdirSync(dirPath); + for (const file of files) { + const filePath = path.join(dirPath, file); + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + getAllFiles(filePath, acc); + } else { + acc.push(filePath); + } + } + return acc; +} + +export function fileExists(filePath: string): boolean { + return fs.existsSync(filePath); +} + +export function readFileContent(filePath: string): string { + return fs.readFileSync(filePath, 'utf-8'); +} + +export function readFileBuffer(filePath: string): Buffer { + return fs.readFileSync(filePath); +} diff --git a/src/genai.ts b/src/genai.ts new file mode 100644 index 0000000..1f72d99 --- /dev/null +++ b/src/genai.ts @@ -0,0 +1,141 @@ +import { GoogleGenAI } from '@google/genai'; +import { ProxyAgent, setGlobalDispatcher } from 'undici'; + +// ---------- URL Helpers ---------- + +export function getUrlOrigin(url: string): string | null { + try { + return new URL(url).origin; + } catch (error) { + console.warn(`[GenAI] Invalid URL for upload origin: ${url}`, error); + return null; + } +} + +function rewriteUploadUrlHost(originalUrl: string, preferredOrigin: string): string { + try { + const target = new URL(originalUrl); + const preferred = new URL(preferredOrigin); + target.protocol = preferred.protocol; + target.hostname = preferred.hostname; + target.port = preferred.port; + return target.toString(); + } catch (error) { + console.warn(`[GenAI] Failed to rewrite upload URL ${originalUrl}:`, error); + return originalUrl; + } +} + +export function forceUploadBaseForFiles(aiClient: GoogleGenAI, preferredOrigin: string) { + const filesAny = aiClient.files as unknown as { + apiClient?: { + fetchUploadUrl?: (...args: unknown[]) => Promise; + __uploadBasePatched?: boolean; + }; + }; + const apiClient = filesAny?.apiClient; + if (!apiClient || typeof apiClient.fetchUploadUrl !== 'function') { + console.warn('[GenAI] Unable to patch upload origin; apiClient missing fetchUploadUrl.'); + return; + } + if (apiClient.__uploadBasePatched) { + return; + } + + const originalFetch = apiClient.fetchUploadUrl.bind(apiClient); + apiClient.fetchUploadUrl = async (...args: unknown[]) => { + const uploadUrl = await originalFetch(...args); + const rewritten = rewriteUploadUrlHost(uploadUrl, preferredOrigin); + if (uploadUrl !== rewritten) { + console.log(`[GenAI] Upload URL rewritten to ${rewritten}`); + } + return rewritten; + }; + + apiClient.__uploadBasePatched = true; +} + +// ---------- Proxy Setup ---------- + +export function setupProxy(): void { + const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY; + if (proxyUrl) { + const proxyAgent = new ProxyAgent(proxyUrl); + setGlobalDispatcher(proxyAgent); + console.log(`[Proxy] Using proxy: ${proxyUrl}`); + } +} + +// ---------- AI Client Factory ---------- + +// Create AI client with custom API key (for per-request usage) +export function createAIClientWithKey(apiKey: string): GoogleGenAI { + const baseUrl = process.env.GEMINI_BASE_URL; + const uploadBaseUrl = process.env.GEMINI_UPLOAD_BASE_URL || baseUrl; + const uploadBaseOrigin = baseUrl ? getUrlOrigin(uploadBaseUrl || baseUrl) : undefined; + + const ai = new GoogleGenAI({ + apiKey, + httpOptions: baseUrl ? { baseUrl } : undefined, + }); + + if (uploadBaseOrigin) { + forceUploadBaseForFiles(ai, uploadBaseOrigin); + } + + return ai; +} + +// Create AI client with environment API key (default/fallback) +export function createAIClient(): GoogleGenAI { + const apiKey = process.env.GEMINI_API_KEY || ''; + if (!apiKey) { + console.error('Error: GEMINI_API_KEY environment variable is not set'); + process.exit(1); + } + + const baseUrl = process.env.GEMINI_BASE_URL; + const uploadBaseUrl = process.env.GEMINI_UPLOAD_BASE_URL || baseUrl; + const uploadBaseOrigin = baseUrl ? getUrlOrigin(uploadBaseUrl || baseUrl) : undefined; + + if (baseUrl) { + console.log(`[GenAI] Using base URL: ${baseUrl}`); + } + if (uploadBaseOrigin) { + console.log(`[GenAI] Rewriting upload origin to: ${uploadBaseOrigin}`); + } + + const ai = new GoogleGenAI({ + apiKey, + httpOptions: baseUrl ? { baseUrl } : undefined, + }); + + if (uploadBaseOrigin) { + forceUploadBaseForFiles(ai, uploadBaseOrigin); + } + + return ai; +} + +// ---------- Connection Test ---------- + +export async function testConnection(ai: GoogleGenAI): Promise { + console.log(`[${new Date().toISOString()}] Checking AI connection...`); + try { + const models = await ai.models.list(); + console.log( + `[${new Date().toISOString()}] AI Connection successful. Models available (first page): ${ + models.page?.length || 0 + }` + ); + + console.log(`[${new Date().toISOString()}] Testing Interactions API...`); + await ai.interactions.create({ + model: 'gemini-3-flash-preview', + input: [{ type: 'text', text: 'Hello' }], + }); + console.log(`[${new Date().toISOString()}] Interactions API test successful.`); + } catch (error) { + console.error(`[${new Date().toISOString()}] AI Connection failed:`, error); + } +} diff --git a/src/index.ts b/src/index.ts index d43e236..b61e564 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,325 +1,37 @@ -#!/usr/bin/env node +import path from 'path'; +import { fileURLToPath } from 'url'; +import { setupProxy, createAIClient, testConnection } from './genai.js'; +import { initializeStore } from './store.js'; +import { createServer, startServer } from './server.js'; -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 { GoogleGenerativeAI } from "@google/generative-ai"; -import { GoogleAIFileManager, FileMetadataResponse } from "@google/generative-ai/server"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import { z } from "zod"; - -// ES module compatibility const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const rootDir = path.dirname(__dirname); -// Schema for documentation query -const DocQuerySchema = z.object({ - query: z.string().describe("The question to ask about the MoonBit documentation"), -}); - -// Schema for source code query -const SourceQuerySchema = z.object({ - query: z.string().describe("The question to ask about MoonBit source code, API, or latest features"), -}); - -class MoonverseMCPServer { - private server: Server; - private genAI: GoogleGenerativeAI; - private fileManager: GoogleAIFileManager; - private uploadedStoreFiles: FileMetadataResponse[] = []; - private uploadedSourceFiles: FileMetadataResponse[] = []; - private apiKey: string; - - constructor() { - // Get API key from environment variable - this.apiKey = process.env.GEMINI_API_KEY || ""; - if (!this.apiKey) { - console.error("Error: GEMINI_API_KEY environment variable is not set"); - process.exit(1); - } - - this.genAI = new GoogleGenerativeAI(this.apiKey); - this.fileManager = new GoogleAIFileManager(this.apiKey); - - this.server = new Server( - { - name: "moonverse-mcp-server", - version: "2.0.0", - }, - { - capabilities: { - tools: {}, - }, - } - ); - - this.setupHandlers(); - } - - private setupHandlers() { - // List available tools - this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [ - { - name: "query_moonbit_docs", - description: "Query the MoonBit documentation using AI-powered semantic search. Ask questions about MoonBit language features, syntax, best practices, and more from the official documentation.", - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "The question to ask about the MoonBit documentation", - }, - }, - required: ["query"], - }, - }, - { - name: "query_moonbit_source", - description: "Query the MoonBit core source code repository to understand latest API features, implementations, and pre-release functionality. Use this for questions about source code, internal implementations, or bleeding-edge features.", - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "The question to ask about MoonBit source code or latest features", - }, - }, - required: ["query"], - }, - }, - ], - })); - - // Handle tool calls - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - if (request.params.name === "query_moonbit_docs") { - const args = DocQuerySchema.parse(request.params.arguments); - return await this.handleDocQuery(args.query); - } else if (request.params.name === "query_moonbit_source") { - const args = SourceQuerySchema.parse(request.params.arguments); - return await this.handleSourceQuery(args.query); - } - - throw new Error(`Unknown tool: ${request.params.name}`); - }); - } - - private async initializeFiles(dirName: string, fileList: FileMetadataResponse[]) { - console.error(`Initializing files from ${dirName}...`); - - // Use the directory where the package is installed - const dir = path.join(path.dirname(__dirname), dirName); - - if (!fs.existsSync(dir)) { - console.error(`${dirName} directory not found. Skipping...`); - return; - } - - // Read all files from directory (including subdirectories) - const files = this.getAllFiles(dir); - const validFiles = files.filter(file => { - const ext = path.extname(file).toLowerCase(); - return [".txt", ".md", ".mbt", ".json"].includes(ext); - }); - - if (validFiles.length === 0) { - console.error(`No valid files found in ${dirName} directory`); - return; - } - - console.error(`Found ${validFiles.length} files to upload from ${dirName}`); - - // Upload files to Gemini using Interactions API - for (const filePath of validFiles) { - const relativePath = path.relative(dir, filePath); - console.error(`Uploading ${relativePath}...`); - - const fileContent = fs.readFileSync(filePath); - const ext = path.extname(filePath).toLowerCase(); - let mimeType = "text/plain"; - if (ext === ".md") mimeType = "text/markdown"; - else if (ext === ".json") mimeType = "application/json"; - else if (ext === ".mbt") mimeType = "text/plain"; - - const uploadResult = await this.fileManager.uploadFile(fileContent, { - mimeType: mimeType, - displayName: relativePath, - }); - - fileList.push(uploadResult.file); - console.error(`Uploaded ${relativePath} - URI: ${uploadResult.file.uri}`); - } - - console.error(`Initialized ${fileList.length} files from ${dirName}`); - } - - private getAllFiles(dirPath: string, arrayOfFiles: string[] = []): string[] { - const files = fs.readdirSync(dirPath); - - files.forEach((file) => { - const filePath = path.join(dirPath, file); - if (fs.statSync(filePath).isDirectory()) { - arrayOfFiles = this.getAllFiles(filePath, arrayOfFiles); - } else { - arrayOfFiles.push(filePath); - } - }); - - return arrayOfFiles; - } - - private async handleDocQuery(query: string) { - try { - // Initialize store files if not already done - if (this.uploadedStoreFiles.length === 0) { - await this.initializeFiles("store", this.uploadedStoreFiles); - } - - if (this.uploadedStoreFiles.length === 0) { - return { - content: [ - { - type: "text", - text: "No documentation files available. Please add documentation files to the 'store' directory.", - }, - ], - isError: true, - }; - } - - console.error(`Processing documentation query: ${query}`); - - // Use Gemini Interactions API with file context - const model = this.genAI.getGenerativeModel({ - model: "gemini-1.5-flash", - }); - - // Create prompt with file context - const prompt = `Based on the MoonBit documentation files provided, please answer the following question: - -${query} - -Please provide a detailed and accurate answer based only on the information in the documentation.`; - - const result = await model.generateContent([ - ...this.uploadedStoreFiles.map(file => ({ - fileData: { - mimeType: file.mimeType, - fileUri: file.uri, - }, - })), - { text: prompt }, - ]); - - const response = result.response; - const text = response.text(); - - return { - content: [ - { - type: "text", - text: text, - }, - ], - }; - } catch (error) { - console.error("Error processing documentation query:", error); - return { - content: [ - { - type: "text", - text: `Error: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } - } - - private async handleSourceQuery(query: string) { - try { - // Initialize source files if not already done - if (this.uploadedSourceFiles.length === 0) { - await this.initializeFiles("source", this.uploadedSourceFiles); - } - - if (this.uploadedSourceFiles.length === 0) { - return { - content: [ - { - type: "text", - text: "No source code files available. Please add the MoonBit core repository to the 'source' directory.", - }, - ], - isError: true, - }; - } - - console.error(`Processing source code query: ${query}`); - - // Use Gemini Interactions API with source code context - const model = this.genAI.getGenerativeModel({ - model: "gemini-1.5-flash", - }); - - // Create prompt focused on source code and implementation - const prompt = `Based on the MoonBit source code repository provided, please answer the following question about the implementation, API, or latest features: - -${query} +// ---------- Main ---------- -Please provide a detailed answer based on the source code. Include relevant code references, API details, and implementation insights where applicable.`; +async function main() { + // Setup proxy if configured + setupProxy(); - const result = await model.generateContent([ - ...this.uploadedSourceFiles.map(file => ({ - fileData: { - mimeType: file.mimeType, - fileUri: file.uri, - }, - })), - { text: prompt }, - ]); + // Create AI client + const ai = createAIClient(); - const response = result.response; - const text = response.text(); + // Test connection + testConnection(ai); - return { - content: [ - { - type: "text", - text: text, - }, - ], - }; - } catch (error) { - console.error("Error processing source query:", error); - return { - content: [ - { - type: "text", - text: `Error: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } - } + // Create and start HTTP server + const port = Number(process.env.PORT || 8080); + const server = createServer(ai, rootDir); + startServer(server, port); - async run() { - const transport = new StdioServerTransport(); - await this.server.connect(transport); - console.error("Moonverse MCP Server running on stdio"); - } + // Initialize store files in background + initializeStore(ai, rootDir).catch((error) => { + console.error('[Startup] Failed to initialize store files:', error); + }); } -// Main execution -const server = new MoonverseMCPServer(); -server.run().catch((error) => { - console.error("Server error:", error); +main().catch((error) => { + console.error('Fatal error:', error); process.exit(1); }); diff --git a/src/query.ts b/src/query.ts new file mode 100644 index 0000000..2f8e28c --- /dev/null +++ b/src/query.ts @@ -0,0 +1,422 @@ +import { GoogleGenAI, createPartFromUri } from '@google/genai'; +import fs from 'fs'; +import path from 'path'; +import { QueryKind, Task, TaskProgress, TaskPhase } from './types.js'; +import { getUploadedStoreFiles } from './store.js'; +import { searchSourceCode, searchSourceCodeRaw } from './search.js'; +import { log, truncateText } from './utils.js'; + +// ---------- Standard Library Directory Structure ---------- + +/** + * Generate a concise directory structure of the MoonBit core library. + * This helps LLM understand what packages are available. + */ +function generateCoreLibraryStructure(rootDir: string): string { + const corePath = path.join(rootDir, 'source', 'core'); + const packages: string[] = []; + + function findPackages(dir: string, prefix: string = '') { + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const hasPkg = entries.some((e) => e.name === 'moon.pkg.json'); + + if (hasPkg && prefix) { + packages.push(prefix); + } + + for (const entry of entries) { + if ( + entry.isDirectory() && + !entry.name.startsWith('.') && + entry.name !== 'target' && + entry.name !== 'internal' + ) { + const newPrefix = prefix ? `${prefix}/${entry.name}` : entry.name; + findPackages(path.join(dir, entry.name), newPrefix); + } + } + } catch (e) { + // Ignore errors + } + } + + findPackages(corePath); + + if (packages.length === 0) { + return ''; + } + + // Group packages by top-level category + const grouped: Map = new Map(); + for (const pkg of packages.sort()) { + const parts = pkg.split('/'); + const category = parts[0]; + if (!grouped.has(category)) { + grouped.set(category, []); + } + if (parts.length > 1) { + grouped.get(category)!.push(parts.slice(1).join('/')); + } + } + + // Build concise structure text + let structure = `MoonBit 标准库包列表 (moonbitlang/core): +`; + + for (const [category, subPackages] of grouped) { + if (subPackages.length === 0) { + structure += ` @${category}\n`; + } else { + structure += ` @${category} (子包: ${subPackages.join(', ')})\n`; + } + } + + structure += ` +提示: 使用 \`moon doc "@包名"\` 可以查看包的详细 API,例如 \`moon doc "@json"\` 或 \`moon doc "String::*"\`。 +`; + + return structure; +} + +// ---------- Task Progress Update ---------- + +// Global task reference for progress updates +let currentTask: Task | null = null; + +export function setCurrentTask(task: Task) { + currentTask = task; +} + +export function updateTaskProgress( + phase: TaskPhase, + message: string, + extra?: Partial +) { + if (currentTask) { + currentTask.progress = { + phase, + message, + ...extra, + }; + currentTask.updatedAt = Date.now(); + console.log(`[${new Date().toISOString()}] Progress: [${phase}] ${message}`); + } +} + +// ---------- Query Execution ---------- + +export async function runQuery( + ai: GoogleGenAI, + rootDir: string, + kind: QueryKind, + query: string, + task?: Task +): Promise { + // Set current task for progress updates + if (task) { + setCurrentTask(task); + } + + console.log( + `[${new Date().toISOString()}] Starting query processing. Kind: ${kind}, Query: "${query}"` + ); + + // For source code queries, use local search + LLM + if (kind === 'source') { + return await searchSourceCode(ai, rootDir, query); + } + + // For hybrid queries, combine docs + code search + if (kind === 'hybrid') { + return await runHybridQuery(ai, rootDir, query); + } + + // For docs queries, use Gemini with uploaded Markdown files + return await runDocsQuery(ai, query); +} + +// ---------- Docs Query ---------- + +async function runDocsQuery(ai: GoogleGenAI, query: string): Promise { + updateTaskProgress('searching-docs', '正在准备文档文件...'); + + const files = getUploadedStoreFiles(); + if (files.length === 0) { + log('[文档查询] 没有找到文档文件'); + return "没有可用的文档文件。请在 'store' 目录中添加 Markdown 文件。"; + } + + log(`[文档查询] 准备使用 ${files.length} 个文档文件:`); + for (const file of files) { + log(` - ${file.name}: ${file.mimeType}, URI: ${truncateText(file.uri, 50)}`); + } + + updateTaskProgress('generating', `正在使用 ${files.length} 个文档文件查询 Gemini...`, { + docsResultsCount: files.length, + docsFiles: files.map((f) => f.name.split('/').pop() || f.name), + partialContent: `文档: ${files.map((f) => f.name.split('/').pop()).join(', ')}`, + }); + + // Build parts array for generateContent + const parts: any[] = []; + for (const file of files) { + parts.push(createPartFromUri(file.uri, file.mimeType)); + } + + const promptText = `根据提供的 MoonBit 文档文件,请回答以下问题: + +${query} + +请用中文提供详细且准确的回答,仅基于文档中的信息。代码示例请使用 \`\`\`moonbit 代码块。`; + + parts.push({ text: promptText }); + + updateTaskProgress('generating', '正在等待 Gemini 生成回答...', { + docsResultsCount: files.length, + partialContent: '正在分析文档内容并组织答案...', + }); + + log(`[文档查询] 发送请求到 Gemini, prompt 长度: ${promptText.length}`); + log(`[文档查询] Prompt: ${truncateText(promptText, 100)}`); + const response = await ai.models.generateContent({ + model: 'gemini-3-flash-preview', + contents: parts, + }); + const responseText = response.text || '未生成回答'; + log(`[文档查询] 收到 Gemini 回答, 长度: ${responseText.length}`); + log(`[文档查询] 回答内容: ${truncateText(responseText, 100)}`); + + updateTaskProgress('complete', '回答生成完成'); + return responseText; +} + +// ---------- Query Classification ---------- + +type QueryType = 'tutorial' | 'api-lookup' | 'syntax' | 'general'; + +/** + * Classify query to determine what context is needed. + * - tutorial: Wants step-by-step guide, doesn't need code search + * - api-lookup: Looking for specific API/package info, needs stdlib list + * - syntax: Asking about operators/syntax, needs code examples + * - general: General question, docs only + */ +function classifyQuery(query: string): QueryType { + const lowerQuery = query.toLowerCase(); + + // Tutorial patterns - explicit step-by-step requests + if ( + /教程|入门|学习|怎么写|完整.*步骤|从.*开始|tutorial|guide|how to (write|build|create)/i.test( + query + ) + ) { + return 'tutorial'; + } + + // API lookup patterns - looking for specific functionality + if (/哪些.*包|什么包|有没有.*函数|api|库|标准库|package|module|@\w+/i.test(query)) { + return 'api-lookup'; + } + + // Syntax patterns - operators, special symbols + if (/[.]{2,3}|[|]>|\?\?|语法|操作符|符号|operator|syntax/i.test(query)) { + return 'syntax'; + } + + return 'general'; +} + +// ---------- Hybrid Query (Docs + Code) ---------- + +async function runHybridQuery(ai: GoogleGenAI, rootDir: string, query: string): Promise { + // Initialize stats + const stats = { llmCalls: 0, searchCalls: 0, toolCalls: 0 }; + + // Step 0: Classify query to determine what context is needed + const queryType = classifyQuery(query); + log(`[混合查询] 查询分类: ${queryType}, query: "${query}"`); + + updateTaskProgress('initializing', `正在分析查询类型: ${queryType}...`, { stats }); + + // Determine what context to include based on query type + const needsCodeSearch = queryType === 'syntax' || queryType === 'api-lookup'; + const needsStdlibList = queryType === 'api-lookup'; + + let codeResults = { + totalMatches: 0, + files: [] as string[], + rawContent: '', + codeDetails: [] as any[], + docDetails: [] as any[], + }; + + // Step 1: Only search code if needed + if (needsCodeSearch) { + updateTaskProgress('searching-code', '正在搜索源代码中的相关示例...', { stats }); + log(`[混合查询] 开始搜索代码, query: "${query}"`); + stats.searchCalls++; + codeResults = await searchSourceCodeRaw(rootDir, query); + log( + `[混合查询] 代码搜索完成: ${codeResults.totalMatches} 个匹配, ${codeResults.files.length} 个文件` + ); + + // Limit code content to avoid bloating the prompt + if (codeResults.rawContent && codeResults.rawContent.length > 8000) { + log(`[混合查询] rawContent 过长 (${codeResults.rawContent.length}),截断到 8000 字符`); + codeResults.rawContent = codeResults.rawContent.substring(0, 8000) + '\n... (更多内容已省略)'; + } + + updateTaskProgress('searching-code', `找到 ${codeResults.totalMatches} 个代码匹配`, { + codeResultsCount: codeResults.totalMatches, + codeFiles: codeResults.files, + codeSearchDetails: codeResults.codeDetails, + docsSearchDetails: codeResults.docDetails, + stats, + partialContent: + codeResults.totalMatches > 0 + ? `匹配文件: ${codeResults.files.slice(0, 3).join(', ')}${codeResults.files.length > 3 ? ' 等' : ''}` + : '未找到代码匹配', + }); + } else { + log(`[混合查询] 跳过代码搜索 (queryType=${queryType})`); + } + + // Step 2: Get docs files + updateTaskProgress('searching-docs', '正在准备文档上下文...', { stats }); + const files = getUploadedStoreFiles(); + log(`[混合查询] 文档文件数: ${files.length}`); + + // Combine uploaded docs with local doc search results + const uploadedDocsDetails = files.map((f) => ({ + name: f.name.split('/').pop() || f.name, + type: f.mimeType, + preview: '(上传的文档文件)', + })); + const allDocsDetails = [...codeResults.docDetails, ...uploadedDocsDetails]; + + updateTaskProgress( + 'analyzing', + needsCodeSearch + ? `正在结合 ${codeResults.totalMatches} 个代码示例和 ${files.length} 个文档文件...` + : `正在使用 ${files.length} 个文档文件生成回答...`, + { + codeResultsCount: codeResults.totalMatches, + docsResultsCount: files.length + codeResults.docDetails.length, + codeFiles: codeResults.files, + docsFiles: files.map((f) => f.name.split('/').pop() || f.name), + codeSearchDetails: codeResults.codeDetails, + docsSearchDetails: allDocsDetails, + stats, + partialContent: '正在整合信息...', + } + ); + + // Step 3: Build prompt based on query type + const parts: any[] = []; + + // Add documentation files + for (const file of files) { + parts.push(createPartFromUri(file.uri, file.mimeType)); + } + + // Build the prompt - keep it minimal and focused + // System constraint: only answer programming-related questions + let promptText = `你是一位 MoonBit 编程语言专家。 + +**重要约束**:你只回答与 MoonBit 编程语言相关的问题。如果用户的问题与编程无关(如闲聊、其他领域的问题),请礼貌地拒绝并说明你只能回答 MoonBit 编程相关的问题。 + +用户正在询问: + +"${query}" + +`; + + // Only add stdlib list for api-lookup queries + if (needsStdlibList) { + const coreLibStructure = generateCoreLibraryStructure(rootDir); + if (coreLibStructure) { + promptText += `${coreLibStructure} +`; + } + } + + // Only add code results if we searched and found something + if (needsCodeSearch && codeResults.rawContent) { + promptText += `以下是在 MoonBit 核心库中找到的相关代码示例: + +${codeResults.rawContent} + +`; + } + + // Tailored instructions based on query type + switch (queryType) { + case 'tutorial': + promptText += `请基于文档提供一个清晰的分步教程: + +1. 从最基础的概念开始解释 +2. 提供完整可运行的代码示例 +3. 每一步都解释清楚原理 +4. 使用 \`\`\`moonbit 代码块 + +注意:专注于教学,保持示例简单明了。`; + break; + + case 'api-lookup': + promptText += `请基于文档和包列表回答: + +1. 指出相关的标准库包 +2. 说明主要的 API 和用法 +3. 提供简短的代码示例 +4. 使用 \`\`\`moonbit 代码块`; + break; + + case 'syntax': + promptText += `请基于文档和代码示例回答: + +1. 解释该语法/操作符的含义 +2. 列出所有不同的用法场景 +3. 展示代码示例 +4. 使用 \`\`\`moonbit 代码块`; + break; + + default: + promptText += `请基于文档用中文回答,使用 \`\`\`moonbit 代码块展示代码示例。`; + } + + parts.push({ text: promptText }); + + updateTaskProgress('generating', '正在使用 Gemini 生成回答...', { + codeResultsCount: codeResults.totalMatches, + docsResultsCount: files.length + codeResults.docDetails.length, + codeFiles: codeResults.files, + docsFiles: files.map((f) => f.name.split('/').pop() || f.name), + codeSearchDetails: codeResults.codeDetails, + docsSearchDetails: allDocsDetails, + stats, + partialContent: `查询类型: ${queryType}`, + }); + + log(`[混合查询] 发送请求到 Gemini, prompt 长度: ${promptText.length}, 类型: ${queryType}`); + log(`[混合查询] Prompt 预览: ${truncateText(promptText, 200)}`); + stats.llmCalls++; + const response = await ai.models.generateContent({ + model: 'gemini-3-flash-preview', + contents: parts, + }); + const responseText = response.text || '未生成回答'; + log(`[混合查询] 收到 Gemini 回答, 长度: ${responseText.length}`); + + updateTaskProgress('complete', '回答生成完成', { + codeResultsCount: codeResults.totalMatches, + docsResultsCount: files.length + codeResults.docDetails.length, + codeFiles: codeResults.files, + docsFiles: files.map((f) => f.name.split('/').pop() || f.name), + codeSearchDetails: codeResults.codeDetails, + docsSearchDetails: allDocsDetails, + stats, + }); + + return responseText; +} diff --git a/src/search.ts b/src/search.ts new file mode 100644 index 0000000..5c0da65 --- /dev/null +++ b/src/search.ts @@ -0,0 +1,422 @@ +import path from 'path'; +import { GoogleGenAI } from '@google/genai'; +import { SearchResult } from './types.js'; +import { getAllFiles, fileExists, readFileContent } from './files.js'; +import { updateTaskProgress } from './query.js'; +import { log, truncateText } from './utils.js'; + +// ---------- Search Result Types ---------- + +export interface CodeFileDetail { + path: string; + matchCount: number; + preview: string; +} + +export interface DocFileDetail { + name: string; + type: string; + preview: string; +} + +export interface RawSearchResults { + totalMatches: number; + files: string[]; + rawContent: string; + codeDetails: CodeFileDetail[]; // Detailed info for each code file + docDetails: DocFileDetail[]; // Detailed info for each doc file +} + +// ---------- Query Preprocessing ---------- + +/** + * Preprocess query to handle special characters and symbols + * Returns both literal patterns and semantic terms + */ +function preprocessQuery(query: string): { + literalPatterns: string[]; + semanticTerms: string[]; + isSymbolQuery: boolean; +} { + const trimmed = query.trim(); + + // Check if this is primarily a symbol/operator query + const symbolPatterns = [ + /^\.\.$/, // exactly ".." + /^\.\.\.$/, // exactly "..." + /^[+\-*\/%&|^~<>=!]+$/, // operators + /^::/, // method access + /^\|>/, // pipe operator + ]; + + const isSymbolQuery = symbolPatterns.some((p) => p.test(trimmed)); + + const literalPatterns: string[] = []; + const semanticTerms: string[] = []; + + if (isSymbolQuery || trimmed.length <= 3) { + // For short/symbol queries, search literally + literalPatterns.push(trimmed); + + // Also add semantic context based on the symbol + if (trimmed === '..') { + semanticTerms.push('range', 'cascade', 'update', 'method chain'); + } else if (trimmed === '...') { + semanticTerms.push('spread', 'rest', 'array spread'); + } else if (trimmed === '|>') { + semanticTerms.push('pipe', 'pipeline'); + } + } else { + // Normal query: split into words but preserve quoted phrases + const words = trimmed + .toLowerCase() + .split(/\s+/) + .filter((t) => t.length > 0); + semanticTerms.push(...words); + + // Also keep the original as a literal pattern if it contains special chars + if (/[^\w\s]/.test(trimmed)) { + literalPatterns.push(trimmed); + } + } + + return { literalPatterns, semanticTerms, isSymbolQuery }; +} + +// ---------- Local Search ---------- + +export function searchInDirectory( + baseDir: string, + extensions: string[], + queryTerms: string[], + literalPatterns: string[] = [], + maxResults: number = 15 +): SearchResult[] { + if (!fileExists(baseDir)) { + return []; + } + + const results: SearchResult[] = []; + const allFiles = getAllFiles(baseDir); + const targetFiles = allFiles.filter((file) => + extensions.includes(path.extname(file).toLowerCase()) + ); + + for (const filePath of targetFiles) { + try { + const content = readFileContent(filePath); + const contentLower = content.toLowerCase(); + + // Check for literal pattern matches first (exact string) + let hasLiteralMatch = false; + if (literalPatterns.length > 0) { + hasLiteralMatch = literalPatterns.some((pattern) => content.includes(pattern)); + } + + // Check for semantic term matches + const hasSemanticMatch = + queryTerms.length > 0 && queryTerms.some((term) => contentLower.includes(term)); + + if (!hasLiteralMatch && !hasSemanticMatch) continue; + + const relativePath = path.relative(baseDir, filePath); + const lines = content.split('\n'); + + // Find matching lines with context + const matchingLines: string[] = []; + const matchedLineIndices = new Set(); + + lines.forEach((line, idx) => { + // Check literal patterns (case-sensitive for symbols) + const hasLiteral = literalPatterns.some((pattern) => line.includes(pattern)); + // Check semantic terms (case-insensitive) + const lineLower = line.toLowerCase(); + const hasSemantic = queryTerms.some((term) => lineLower.includes(term)); + + if (hasLiteral || hasSemantic) { + matchedLineIndices.add(idx); + } + }); + + // Add context around matched lines + for (const idx of matchedLineIndices) { + const start = Math.max(0, idx - 2); + const end = Math.min(lines.length, idx + 3); + for (let i = start; i < end; i++) { + const prefix = i === idx ? '>>> ' : ' '; + const lineContent = `${prefix}Line ${i + 1}: ${lines[i]}`; + if (!matchingLines.includes(lineContent)) { + matchingLines.push(lineContent); + } + } + matchingLines.push(''); // separator + } + + if (matchingLines.length > 0) { + results.push({ + relativePath, + matchingLines: matchingLines.slice(0, 50), + fullContent: content.length < 8000 ? content : undefined, + }); + } + + if (results.length >= maxResults) break; + } catch (error) { + // Skip files that can't be read + } + } + + return results; +} + +// ---------- Raw Search (for hybrid queries) ---------- + +export async function searchSourceCodeRaw( + rootDir: string, + query: string +): Promise { + console.log(`[${new Date().toISOString()}] Raw search for: "${query}"`); + + const sourceDir = path.join(rootDir, 'source'); + if (!fileExists(sourceDir)) { + return { totalMatches: 0, files: [], rawContent: '', codeDetails: [], docDetails: [] }; + } + + const { literalPatterns, semanticTerms, isSymbolQuery } = preprocessQuery(query); + + console.log( + `[${new Date().toISOString()}] Query analysis: literal=[${literalPatterns.join( + ', ' + )}], semantic=[${semanticTerms.join(', ')}], isSymbol=${isSymbolQuery}` + ); + + // Search in MoonBit core + const coreDir = path.join(sourceDir, 'core'); + const coreResults = searchInDirectory(coreDir, ['.mbt'], semanticTerms, literalPatterns, 20); + + // Search documentation + const docResults = searchInDirectory(sourceDir, ['.md'], semanticTerms, literalPatterns, 10); + + // Combine results + let rawContent = ''; + const files: string[] = []; + const codeDetails: CodeFileDetail[] = []; + const docDetails: DocFileDetail[] = []; + + if (coreResults.length > 0) { + rawContent += '\n## MoonBit Core Source Code\n'; + log(`[代码搜索] 找到 ${coreResults.length} 个 Core 文件:`); + for (const r of coreResults) { + files.push(r.relativePath); + const content = r.matchingLines.join('\n'); + const preview = truncateText(content, 200); + log(` - ${r.relativePath}: ${r.matchingLines.length} 行匹配`); + log(` 内容: ${truncateText(content, 100)}`); + rawContent += `\n### ${r.relativePath}\n\`\`\`moonbit\n${content}\n\`\`\`\n`; + + // Add detailed info + codeDetails.push({ + path: r.relativePath, + matchCount: + r.matchingLines.filter((l) => l.startsWith('>>>')).length || r.matchingLines.length, + preview, + }); + } + } else { + log(`[代码搜索] Core 目录未找到匹配`); + } + + if (docResults.length > 0) { + rawContent += '\n## Documentation\n'; + log(`[文档搜索] 找到 ${docResults.length} 个文档文件:`); + for (const r of docResults) { + files.push(r.relativePath); + const content = r.matchingLines.slice(0, 20).join('\n'); + const preview = truncateText(content, 200); + log(` - ${r.relativePath}: ${r.matchingLines.length} 行匹配`); + log(` 内容: ${truncateText(content, 100)}`); + rawContent += `\n### ${r.relativePath}\n${content}\n`; + + // Add doc details with preview + docDetails.push({ + name: r.relativePath, + type: 'text/markdown', + preview, + }); + } + } else { + log(`[文档搜索] 未找到匹配的文档`); + } + + log( + `[搜索汇总] 总计: ${coreResults.length + docResults.length} 个匹配, rawContent 长度: ${rawContent.length}` + ); + + return { + totalMatches: coreResults.length + docResults.length, + files, + rawContent, + codeDetails, + docDetails, + }; +} + +// ---------- Source Code Search with LLM ---------- + +export async function searchSourceCode( + ai: GoogleGenAI, + rootDir: string, + query: string +): Promise { + console.log(`[${new Date().toISOString()}] Searching local code for: "${query}"`); + + updateTaskProgress('searching-code', '正在分析查询并搜索源代码...'); + + const sourceDir = path.join(rootDir, 'source'); + if (!fileExists(sourceDir)) { + return '未找到源代码目录。'; + } + + const { literalPatterns, semanticTerms, isSymbolQuery } = preprocessQuery(query); + + console.log( + `[${new Date().toISOString()}] Query analysis: literal=[${literalPatterns.join( + ', ' + )}], semantic=[${semanticTerms.join(', ')}], isSymbol=${isSymbolQuery}` + ); + + // For symbol queries, we don't require minimum length + if (semanticTerms.length === 0 && literalPatterns.length === 0) { + return '查询太短。请提供更具体的搜索词。'; + } + + // Step 1: Search in genai-sdk-samples for TypeScript examples + updateTaskProgress('searching-code', '正在搜索 SDK 示例...', { + partialContent: '扫描 TypeScript 示例文件...', + }); + const sdkSamplesDir = path.join(sourceDir, 'genai-sdk-samples'); + const sdkResults = searchInDirectory(sdkSamplesDir, ['.ts'], semanticTerms, literalPatterns, 10); + + // Step 2: Search in core for MoonBit source code + updateTaskProgress( + 'searching-code', + `正在搜索 MoonBit 核心库... (已找到 ${sdkResults.length} 个 SDK 示例)`, + { + codeResultsCount: sdkResults.length, + partialContent: '扫描 .mbt 源文件...', + } + ); + const coreDir = path.join(sourceDir, 'core'); + const coreResults = searchInDirectory(coreDir, ['.mbt'], semanticTerms, literalPatterns, 15); + + // Step 3: Search README and documentation files + updateTaskProgress( + 'searching-code', + `正在搜索文档文件... (已找到 ${sdkResults.length + coreResults.length} 个代码匹配)`, + { + codeResultsCount: sdkResults.length + coreResults.length, + partialContent: '扫描 README 和文档...', + } + ); + const docResults = searchInDirectory( + sourceDir, + ['.md', '.json'], + semanticTerms, + literalPatterns, + 8 + ); + + const totalResults = sdkResults.length + coreResults.length + docResults.length; + updateTaskProgress('searching-code', `搜索完成,找到 ${totalResults} 个匹配文件`, { + codeResultsCount: totalResults, + partialContent: + totalResults > 0 + ? `匹配文件: ${[...coreResults, ...docResults] + .slice(0, 3) + .map((r) => r.relativePath) + .join(', ')}${totalResults > 3 ? ' 等' : ''}` + : '未找到匹配', + }); + + // Combine raw results + let rawResults = ''; + + if (sdkResults.length > 0) { + rawResults += '\n## SDK Samples (TypeScript)\n'; + for (const r of sdkResults) { + rawResults += `\n### ${ + r.relativePath + }\n\`\`\`typescript\n${r.matchingLines.join('\n')}\n\`\`\`\n`; + } + } + + if (coreResults.length > 0) { + rawResults += '\n## MoonBit Core Source Code\n'; + for (const r of coreResults) { + rawResults += `\n### ${ + r.relativePath + }\n\`\`\`moonbit\n${r.matchingLines.join('\n')}\n\`\`\`\n`; + } + } + + if (docResults.length > 0) { + rawResults += '\n## Documentation\n'; + for (const r of docResults) { + rawResults += `\n### ${r.relativePath}\n${r.matchingLines.slice(0, 15).join('\n')}\n`; + } + } + + if (!rawResults) { + return `未找到与 "${query}" 匹配的代码。请尝试不同的搜索词。`; + } + + // Step 4: Use LLM to organize and verify the results + updateTaskProgress('generating', '正在使用 AI 分析和整理搜索结果...', { + codeResultsCount: totalResults, + partialContent: `正在处理 ${totalResults} 个匹配文件的内容...`, + }); + + const prompt = `你是 MoonBit 编程语言的文档助手。 + +用户询问: "${query}" + +${ + isSymbolQuery + ? `注意: 这是关于特定符号/操作符 "${query}" 的查询。请重点解释该符号在 MoonBit 中的所有不同用法和含义。` + : '' +} + +以下是从代码库中找到的原始搜索结果: +${rawResults} + +请分析这些搜索结果,用中文提供组织良好的回答: +1. 根据找到的代码直接回答用户的问题 +2. ${ + isSymbolQuery + ? '列出该符号的所有不同用法/含义,并配上示例' + : '包含相关的代码示例,使用正确的语法高亮' + } +3. 如有帮助,解释代码的工作原理 +4. 如果搜索结果不能完全回答问题,请说明并建议用户可以找什么 + +请使用 \`\`\`moonbit 代码块格式化 MoonBit 代码。`; + + try { + const response = await ai.models.generateContent({ + model: 'gemini-3-flash-preview', + contents: prompt, + }); + + const result = response.text; + if (result) { + updateTaskProgress('complete', '搜索和分析完成'); + return result; + } + } catch (error) { + console.error(`[${new Date().toISOString()}] LLM processing failed:`, error); + } + + // Fallback to raw results if LLM fails + updateTaskProgress('complete', '返回原始搜索结果 (LLM 不可用)'); + return `"${query}" 的原始搜索结果:\n${rawResults}`; +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..2a357cf --- /dev/null +++ b/src/server.ts @@ -0,0 +1,681 @@ +import http from 'http'; +import { randomUUID } from 'crypto'; +import { spawn } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import { z } from 'zod'; +import { GoogleGenAI } from '@google/genai'; +import { Task, TaskProgress, DailyUsage, DAILY_LIMIT } from './types.js'; +import { runQuery, updateTaskProgress } from './query.js'; +import { createAIClientWithKey } from './genai.js'; + +// ---------- Global API Usage Tracking ---------- + +let globalUsage: DailyUsage = { + date: new Date().toISOString().split('T')[0], + count: 0, +}; + +function checkAndIncrementGlobalUsage(): { allowed: boolean; remaining: number } { + const today = new Date().toISOString().split('T')[0]; + + // Reset count if it's a new day + if (globalUsage.date !== today) { + globalUsage = { date: today, count: 0 }; + } + + if (globalUsage.count >= DAILY_LIMIT) { + return { allowed: false, remaining: 0 }; + } + + globalUsage.count++; + return { allowed: true, remaining: DAILY_LIMIT - globalUsage.count }; +} + +function getGlobalUsageInfo(): { date: string; used: number; remaining: number } { + const today = new Date().toISOString().split('T')[0]; + if (globalUsage.date !== today) { + return { date: today, used: 0, remaining: DAILY_LIMIT }; + } + return { + date: globalUsage.date, + used: globalUsage.count, + remaining: DAILY_LIMIT - globalUsage.count, + }; +} + +// ---------- IP Rate Limiting (per hour) ---------- + +const IP_HOURLY_LIMIT = 20; + +interface IPUsageRecord { + hour: string; // Format: "YYYY-MM-DD-HH" + count: number; +} + +const ipUsage = new Map(); + +function getCurrentHour(): string { + const now = new Date(); + return `${now.toISOString().split('T')[0]}-${String(now.getUTCHours()).padStart(2, '0')}`; +} + +function getClientIP(req: http.IncomingMessage): string { + // Check X-Forwarded-For header first (for proxied requests) + const forwarded = req.headers['x-forwarded-for']; + if (forwarded) { + const ips = (Array.isArray(forwarded) ? forwarded[0] : forwarded).split(','); + return ips[0].trim(); + } + // Fall back to socket remote address + return req.socket.remoteAddress || 'unknown'; +} + +function checkIPRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number } { + const currentHour = getCurrentHour(); + + // Clean up old entries periodically (simple cleanup: remove if hour changed) + const record = ipUsage.get(ip); + if (record && record.hour !== currentHour) { + ipUsage.delete(ip); + } + + const usage = ipUsage.get(ip); + + if (!usage) { + // First request this hour + ipUsage.set(ip, { hour: currentHour, count: 1 }); + return { allowed: true, remaining: IP_HOURLY_LIMIT - 1, resetIn: getMinutesToNextHour() }; + } + + if (usage.count >= IP_HOURLY_LIMIT) { + return { allowed: false, remaining: 0, resetIn: getMinutesToNextHour() }; + } + + usage.count++; + return { + allowed: true, + remaining: IP_HOURLY_LIMIT - usage.count, + resetIn: getMinutesToNextHour(), + }; +} + +function getMinutesToNextHour(): number { + const now = new Date(); + return 60 - now.getMinutes(); +} + +function getIPUsageInfo(ip: string): { + ip: string; + hour: string; + used: number; + remaining: number; + resetInMinutes: number; +} { + const currentHour = getCurrentHour(); + const record = ipUsage.get(ip); + + if (!record || record.hour !== currentHour) { + return { + ip, + hour: currentHour, + used: 0, + remaining: IP_HOURLY_LIMIT, + resetInMinutes: getMinutesToNextHour(), + }; + } + + return { + ip, + hour: currentHour, + used: record.count, + remaining: IP_HOURLY_LIMIT - record.count, + resetInMinutes: getMinutesToNextHour(), + }; +} + +// ---------- Validation ---------- + +const MAX_QUERY_LENGTH = 1000; + +const CreateQuerySchema = z.object({ + type: z.enum(['docs', 'source', 'hybrid']), + query: z + .string() + .min(1) + .max(MAX_QUERY_LENGTH, { + message: `查询内容过长,最多支持 ${MAX_QUERY_LENGTH} 个字符`, + }), +}); + +const MoonCommandSchema = z.object({ + command: z.string().min(1), // e.g., "doc", "doc @json", "doc String::*rev*" + cwd: z.string().optional(), // Optional working directory, defaults to source/core +}); + +// ---------- Task Management ---------- + +const tasks = new Map(); + +// ---------- CORS Support ---------- + +function setCorsHeaders(res: http.ServerResponse) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key'); + res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours +} + +function respondJson(res: http.ServerResponse, status: number, body: unknown) { + setCorsHeaders(res); + res.statusCode = status; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(body)); +} + +async function handleCreateQuery( + req: http.IncomingMessage, + res: http.ServerResponse, + defaultAI: GoogleGenAI, + rootDir: string +) { + // Extract API key from header + const headerApiKey = req.headers['x-api-key'] as string | undefined; + const clientIP = getClientIP(req); + + // If user provides their own API key, skip IP rate limiting + if (!headerApiKey) { + // Check IP rate limit only for requests using global key + const ipLimit = checkIPRateLimit(clientIP); + if (!ipLimit.allowed) { + console.log(`[${new Date().toISOString()}] IP rate limit exceeded for ${clientIP}`); + respondJson(res, 429, { + error: `IP 请求频率超限,每小时最多 ${IP_HOURLY_LIMIT} 次`, + hint: `请等待 ${ipLimit.resetIn} 分钟后重试,或提供自己的 API Key 绕过限制`, + ipUsage: getIPUsageInfo(clientIP), + }); + return; + } + } + + let raw = ''; + req.on('data', (chunk) => (raw += chunk)); + req.on('end', () => { + try { + const parsed = raw ? JSON.parse(raw) : {}; + const data = CreateQuerySchema.parse(parsed); + + // Determine which AI client to use + let ai: GoogleGenAI; + let usingGlobalKey = false; + + if (headerApiKey) { + // Use user-provided API key - no rate limiting + ai = createAIClientWithKey(headerApiKey); + console.log( + `[${new Date().toISOString()}] Using user-provided API key from IP ${clientIP} (no rate limit)` + ); + } else { + // Check global usage limit + const usage = checkAndIncrementGlobalUsage(); + if (!usage.allowed) { + respondJson(res, 429, { + error: '全局 API 配额已用完,请在请求头中提供 X-API-Key', + hint: '请在 Header 中添加: X-API-Key: your-gemini-api-key', + usage: getGlobalUsageInfo(), + }); + return; + } + ai = defaultAI; + usingGlobalKey = true; + const ipInfo = getIPUsageInfo(clientIP); + console.log( + `[${new Date().toISOString()}] Using global API key from IP ${clientIP} (${usage.remaining} global, ${ipInfo.remaining} IP remaining)` + ); + } + + const id = randomUUID(); + const now = Date.now(); + const task: Task = { + id, + kind: data.type, + query: data.query, + status: 'queued', + createdAt: now, + updatedAt: now, + etaSeconds: 3, + pollCount: 0, + progress: { + phase: 'initializing', + message: '任务已加入队列,等待开始...', + }, + }; + + tasks.set(id, task); + console.log(`[${new Date().toISOString()}] Task created. ID: ${id}, Kind: ${data.type}`); + + processTask(task, ai, rootDir).catch((err) => { + console.error( + `[${new Date().toISOString()}] Task processing failed. ID: ${id}, Error:`, + err + ); + const t = tasks.get(id); + if (t) { + t.status = 'error'; + t.error = err instanceof Error ? err.message : String(err); + t.updatedAt = Date.now(); + } + }); + + const responseBody: any = { id, nextPollSec: 3 }; + if (usingGlobalKey) { + responseBody.globalUsage = getGlobalUsageInfo(); + } + respondJson(res, 202, responseBody); + } catch (error) { + console.error(`[${new Date().toISOString()}] Error handling create query:`, error); + respondJson(res, 400, { + error: error instanceof Error ? error.message : String(error), + }); + } + }); +} + +// ---------- Moon Command Execution ---------- + +async function handleMoonCommand( + req: http.IncomingMessage, + res: http.ServerResponse, + rootDir: string +) { + let raw = ''; + req.on('data', (chunk) => (raw += chunk)); + req.on('end', async () => { + try { + const parsed = raw ? JSON.parse(raw) : {}; + const data = MoonCommandSchema.parse(parsed); + + // Default to source/core directory + const cwd = data.cwd ? path.resolve(rootDir, data.cwd) : path.join(rootDir, 'source', 'core'); + + console.log( + `[${new Date().toISOString()}] Running moon command: moon ${data.command} in ${cwd}` + ); + + // Execute moon command + const result = await runMoonCommand(data.command, cwd); + respondJson(res, 200, result); + } catch (error) { + console.error(`[${new Date().toISOString()}] Error handling moon command:`, error); + respondJson(res, 400, { + error: error instanceof Error ? error.message : String(error), + }); + } + }); +} + +// ---------- Source Tree Listing ---------- + +interface TreeNode { + name: string; + type: 'file' | 'directory'; + path: string; + children?: TreeNode[]; +} + +function buildDirectoryTree( + dirPath: string, + relativeTo: string, + maxDepth: number = 3, + currentDepth: number = 0 +): TreeNode | null { + try { + const stat = fs.statSync(dirPath); + const name = path.basename(dirPath); + const relPath = path.relative(relativeTo, dirPath); + + // Skip hidden files/directories and common non-essential directories + if (name.startsWith('.') || name === 'node_modules' || name === 'target') { + return null; + } + + if (stat.isFile()) { + // Only include relevant file types + const ext = path.extname(name).toLowerCase(); + if (['.mbt', '.mbti', '.md', '.json', '.txt'].includes(ext)) { + return { name, type: 'file', path: relPath }; + } + return null; + } + + if (stat.isDirectory() && currentDepth < maxDepth) { + const entries = fs.readdirSync(dirPath); + const children: TreeNode[] = []; + + for (const entry of entries.sort()) { + const childPath = path.join(dirPath, entry); + const childNode = buildDirectoryTree(childPath, relativeTo, maxDepth, currentDepth + 1); + if (childNode) { + children.push(childNode); + } + } + + // Only include directories that have relevant children or are package roots + const hasPkgJson = entries.includes('moon.pkg.json'); + if (children.length > 0 || hasPkgJson) { + return { name, type: 'directory', path: relPath, children }; + } + } + + return null; + } catch (error) { + console.error(`Error reading ${dirPath}:`, error); + return null; + } +} + +function handleSourceTree(res: http.ServerResponse, rootDir: string) { + console.log(`[${new Date().toISOString()}] Handling source tree request`); + + const sourcePath = path.join(rootDir, 'source'); + + try { + // Build the tree with depth 3 + const tree = buildDirectoryTree(sourcePath, sourcePath, 3); + + // Also provide a flat list of all packages (directories with moon.pkg.json) + const packages: string[] = []; + + function findPackages(dir: string, prefix: string = '') { + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const hasPkg = entries.some((e) => e.name === 'moon.pkg.json'); + + if (hasPkg) { + packages.push(prefix || '(root)'); + } + + for (const entry of entries) { + if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'target') { + const newPrefix = prefix ? `${prefix}/${entry.name}` : entry.name; + findPackages(path.join(dir, entry.name), newPrefix); + } + } + } catch (e) { + // Ignore errors for individual directories + } + } + + findPackages(path.join(rootDir, 'source', 'core')); + + respondJson(res, 200, { + tree, + packages: packages.sort(), + sourcePath: 'source/', + }); + } catch (error) { + console.error(`[${new Date().toISOString()}] Error building source tree:`, error); + respondJson(res, 500, { + error: error instanceof Error ? error.message : String(error), + }); + } +} + +function runMoonCommand( + command: string, + cwd: string +): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number }> { + return new Promise((resolve) => { + // Split command into args (e.g., "doc @json" -> ["doc", "@json"]) + const args = command.split(/\s+/).filter(Boolean); + + console.log(`[${new Date().toISOString()}] Spawning: moon ${args.join(' ')} in ${cwd}`); + + const proc = spawn('moon', args, { + cwd, + env: { ...process.env }, + shell: true, + }); + + let stdout = ''; + let stderr = ''; + + proc.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + proc.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + proc.on('close', (code) => { + console.log(`[${new Date().toISOString()}] Moon command completed with exit code: ${code}`); + resolve({ + success: code === 0, + stdout, + stderr, + exitCode: code ?? 1, + }); + }); + + proc.on('error', (err) => { + console.error(`[${new Date().toISOString()}] Moon command spawn error:`, err); + resolve({ + success: false, + stdout, + stderr: err.message, + exitCode: 1, + }); + }); + + // Timeout after 30 seconds + setTimeout(() => { + proc.kill(); + resolve({ + success: false, + stdout, + stderr: 'Command timed out after 30 seconds', + exitCode: 124, + }); + }, 30000); + }); +} + +async function handleGetQuery(req: http.IncomingMessage, res: http.ServerResponse, id: string) { + console.log(`[${new Date().toISOString()}] Handling get query. ID: ${id}`); + const task = tasks.get(id); + if (!task) { + console.warn(`[${new Date().toISOString()}] Task not found. ID: ${id}`); + respondJson(res, 404, { error: 'task not found' }); + return; + } + + // Increment poll count + task.pollCount++; + const elapsedSeconds = (Date.now() - task.createdAt) / 1000; + + console.log( + `[${new Date().toISOString()}] Task status. ID: ${id}, Status: ${ + task.status + }, Poll #${task.pollCount}` + ); + + if (task.status === 'done') { + const doneResponse: any = { + status: 'done', + content: task.result, + stats: { + pollCount: task.pollCount, + totalTimeSeconds: elapsedSeconds, + }, + }; + // Include final counts and files if available + if (task.progress) { + if (task.progress.codeResultsCount !== undefined) { + doneResponse.codeResultsCount = task.progress.codeResultsCount; + } + if (task.progress.docsResultsCount !== undefined) { + doneResponse.docsResultsCount = task.progress.docsResultsCount; + } + if (task.progress.codeFiles && task.progress.codeFiles.length > 0) { + doneResponse.codeFiles = task.progress.codeFiles; + } + if (task.progress.docsFiles && task.progress.docsFiles.length > 0) { + doneResponse.docsFiles = task.progress.docsFiles; + } + // Include detailed search results + if (task.progress.codeSearchDetails && task.progress.codeSearchDetails.length > 0) { + doneResponse.codeSearchDetails = task.progress.codeSearchDetails; + } + if (task.progress.docsSearchDetails && task.progress.docsSearchDetails.length > 0) { + doneResponse.docsSearchDetails = task.progress.docsSearchDetails; + } + // Include query stats (LLM calls, search calls, etc.) + if (task.progress.stats) { + doneResponse.queryStats = task.progress.stats; + } + } + respondJson(res, 200, doneResponse); + } else if (task.status === 'error') { + respondJson(res, 200, { + status: 'error', + error: task.error, + stats: { + pollCount: task.pollCount, + totalTimeSeconds: elapsedSeconds, + }, + }); + } else { + // Build progress response - dynamic poll interval based on elapsed time + const dynamicPollSec = elapsedSeconds < 5 ? 3 : elapsedSeconds < 15 ? 4 : 5; + const progressResponse: any = { + status: task.status, + etaSeconds: Math.max(1, task.etaSeconds - Math.floor(elapsedSeconds)), + nextPollSec: dynamicPollSec, + pollCount: task.pollCount, + elapsedSeconds: Math.round(elapsedSeconds * 10) / 10, + }; + + // Include progress details if available + if (task.progress) { + progressResponse.phase = task.progress.phase; + progressResponse.message = task.progress.message; + + if (task.progress.codeResultsCount !== undefined) { + progressResponse.codeResultsCount = task.progress.codeResultsCount; + } + if (task.progress.docsResultsCount !== undefined) { + progressResponse.docsResultsCount = task.progress.docsResultsCount; + } + if (task.progress.partialContent) { + progressResponse.partialContent = task.progress.partialContent; + } + // Include file lists + if (task.progress.codeFiles && task.progress.codeFiles.length > 0) { + progressResponse.codeFiles = task.progress.codeFiles; + } + if (task.progress.docsFiles && task.progress.docsFiles.length > 0) { + progressResponse.docsFiles = task.progress.docsFiles; + } + // Include detailed search results + if (task.progress.codeSearchDetails && task.progress.codeSearchDetails.length > 0) { + progressResponse.codeSearchDetails = task.progress.codeSearchDetails; + } + if (task.progress.docsSearchDetails && task.progress.docsSearchDetails.length > 0) { + progressResponse.docsSearchDetails = task.progress.docsSearchDetails; + } + // Include query stats (LLM calls, search calls, etc.) + if (task.progress.stats) { + progressResponse.queryStats = task.progress.stats; + } + } + + respondJson(res, 200, progressResponse); + } +} + +async function processTask(task: Task, ai: GoogleGenAI, rootDir: string) { + console.log(`[${new Date().toISOString()}] Processing task. ID: ${task.id}, Kind: ${task.kind}`); + task.status = 'running'; + task.updatedAt = Date.now(); + task.progress = { + phase: 'initializing', + message: '开始处理查询...', + }; + + try { + const result = await runQuery(ai, rootDir, task.kind, task.query, task); + task.result = result; + task.status = 'done'; + task.updatedAt = Date.now(); + // Keep the progress info (with counts and files) but update phase + if (task.progress) { + task.progress.phase = 'complete'; + task.progress.message = '查询完成'; + } + console.log(`[${new Date().toISOString()}] Task completed. ID: ${task.id}`); + } catch (error) { + console.error(`[${new Date().toISOString()}] Task failed. ID: ${task.id}, Error:`, error); + task.status = 'error'; + task.error = error instanceof Error ? error.message : String(error); + task.updatedAt = Date.now(); + } +} + +// ---------- Server Factory ---------- + +export function createServer(ai: GoogleGenAI, rootDir: string): http.Server { + const server = http.createServer((req, res) => { + const url = req.url || '/'; + const method = req.method || 'GET'; + + // Handle CORS preflight requests + if (method === 'OPTIONS') { + setCorsHeaders(res); + res.statusCode = 204; + res.end(); + return; + } + + if (method === 'GET' && url === '/healthz') { + respondJson(res, 200, { ok: true }); + return; + } + + if (method === 'GET' && url === '/usage') { + respondJson(res, 200, getGlobalUsageInfo()); + return; + } + + if (method === 'POST' && url === '/query') { + handleCreateQuery(req, res, ai, rootDir); + return; + } + + if (method === 'POST' && url === '/moon') { + handleMoonCommand(req, res, rootDir); + return; + } + + if (method === 'GET' && url === '/source-tree') { + handleSourceTree(res, rootDir); + return; + } + + if (method === 'GET' && url.startsWith('/query/')) { + const id = url.split('/query/')[1]; + handleGetQuery(req, res, id); + return; + } + + respondJson(res, 404, { error: 'not found' }); + }); + + return server; +} + +export function startServer(server: http.Server, port: number): void { + server.listen(port, () => { + console.error(`Moonverse HTTP server listening on port ${port}`); + }); +} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..4880dfd --- /dev/null +++ b/src/store.ts @@ -0,0 +1,183 @@ +import path from 'path'; +import { GoogleGenAI } from '@google/genai'; +import { FileInfo, CacheData } from './types.js'; +import { getAllFiles, getMimeType, fileExists, readFileBuffer } from './files.js'; +import { sleep } from './utils.js'; +import { + loadCache, + saveCache, + computeFileHash, + findValidCachedFile, + addOrUpdateCacheEntry, + cleanExpiredEntries, + formatTimeRemaining, +} from './cache.js'; + +// ---------- Store State ---------- + +const uploadedStoreFiles: FileInfo[] = []; +let storeInitialized = false; + +export function getUploadedStoreFiles(): FileInfo[] { + return uploadedStoreFiles; +} + +export function isStoreInitialized(): boolean { + return storeInitialized; +} + +// ---------- File Upload ---------- + +export async function initializeStore(ai: GoogleGenAI, rootDir: string): Promise { + const dirName = 'store'; + console.log(`[${new Date().toISOString()}] Initializing files from ${dirName}...`); + + const dir = path.join(rootDir, dirName); + if (!fileExists(dir)) { + console.error(`[${new Date().toISOString()}] ${dirName} directory not found. Skipping...`); + return; + } + + if (uploadedStoreFiles.length > 0) { + console.log( + `[${new Date().toISOString()}] Files already initialized for ${dirName}. Count: ${ + uploadedStoreFiles.length + }` + ); + return; + } + + // Load cache + const cache: CacheData = loadCache(rootDir); + const expiredCount = cleanExpiredEntries(cache); + if (expiredCount > 0) { + console.log(`[${new Date().toISOString()}] Cleaned ${expiredCount} expired cache entries`); + } + + const files = getAllFiles(dir); + // Upload Markdown and text files to Gemini + const validFiles = files.filter((file) => + ['.md', '.txt'].includes(path.extname(file).toLowerCase()) + ); + + if (validFiles.length === 0) { + console.error(`[${new Date().toISOString()}] No valid files found in ${dirName} directory`); + return; + } + + console.log( + `[${new Date().toISOString()}] Found ${validFiles.length} files to process from ${dirName}` + ); + + let uploadedCount = 0; + let cachedCount = 0; + + for (const filePath of validFiles) { + const relativePath = path.relative(dir, filePath); + const fileHash = computeFileHash(filePath); + + // Check cache first + const cachedEntry = findValidCachedFile(cache, relativePath, fileHash); + if (cachedEntry) { + // Verify the file still exists on Gemini + try { + const remoteFile = await ai.files.get({ name: cachedEntry.name }); + if (remoteFile && remoteFile.state === 'ACTIVE') { + uploadedStoreFiles.push({ + uri: cachedEntry.uri, + mimeType: cachedEntry.mimeType, + name: cachedEntry.name, + }); + cachedCount++; + console.log( + `[${new Date().toISOString()}] ✓ Cache hit: ${relativePath} (${formatTimeRemaining( + cachedEntry.expiresAt + )})` + ); + continue; + } + } catch { + // File no longer exists on Gemini, need to re-upload + console.log( + `[${new Date().toISOString()}] Cache invalid: ${relativePath} - file no longer exists on Gemini` + ); + } + } + + // Upload file + console.log(`[${new Date().toISOString()}] Uploading ${relativePath}...`); + + const fileContent = readFileBuffer(filePath); + const ext = path.extname(filePath).toLowerCase(); + const mimeType = getMimeType(ext); + const blob = new Blob([fileContent], { type: mimeType }); + + try { + console.log(`[${new Date().toISOString()}] Starting upload for ${relativePath}...`); + const uploadedFile = await ai.files.upload({ + file: blob, + config: { + mimeType, + displayName: relativePath, + }, + }); + console.log( + `[${new Date().toISOString()}] Upload request completed for ${relativePath}. Name: ${ + uploadedFile.name + }` + ); + + let processed = await ai.files.get({ + name: uploadedFile.name as string, + }); + let retries = 0; + const maxRetries = 60; + while (processed.state === 'PROCESSING' && retries < maxRetries) { + await sleep(5000); + processed = await ai.files.get({ + name: uploadedFile.name as string, + }); + retries++; + } + + if (processed.state === 'FAILED') { + console.error(`[${new Date().toISOString()}] Failed to process file: ${relativePath}`); + continue; + } + + const fileInfo: FileInfo = { + uri: processed.uri as string, + mimeType: processed.mimeType as string, + name: processed.name as string, + }; + + uploadedStoreFiles.push(fileInfo); + uploadedCount++; + + // Update cache + addOrUpdateCacheEntry(cache, { + uri: fileInfo.uri, + mimeType: fileInfo.mimeType, + name: fileInfo.name, + displayName: relativePath, + fileHash, + }); + + console.log( + `[${new Date().toISOString()}] ✓ Uploaded ${relativePath} - URI: ${processed.uri}` + ); + } catch (error) { + console.error(`[${new Date().toISOString()}] Error uploading ${relativePath}:`, error); + } + } + + // Save updated cache + saveCache(rootDir, cache); + + storeInitialized = true; + console.log( + `[${new Date().toISOString()}] Initialized ${ + uploadedStoreFiles.length + } files from ${dirName} (${cachedCount} from cache, ${uploadedCount} uploaded)` + ); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..55f5982 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,101 @@ +// ---------- Types ---------- + +export interface FileInfo { + uri: string; + mimeType: string; + name: string; +} + +export type QueryKind = 'docs' | 'source' | 'hybrid'; + +export type TaskStatus = 'queued' | 'running' | 'done' | 'error'; + +export type TaskPhase = + | 'initializing' + | 'searching-code' + | 'searching-docs' + | 'analyzing' + | 'generating' + | 'complete'; + +export interface TaskProgress { + phase: TaskPhase; + message: string; + partialContent?: string; // Incremental content available + codeResultsCount?: number; // Number of code matches found + docsResultsCount?: number; // Number of doc matches found + codeFiles?: string[]; // List of matched code files + docsFiles?: string[]; // List of docs files used + // Detailed search results for display + codeSearchDetails?: CodeFileDetail[]; // Detailed code search results + docsSearchDetails?: DocFileDetail[]; // Detailed doc search info + // Statistics + stats?: QueryStats; // Query process statistics +} + +// Detailed info for a matched code file +export interface CodeFileDetail { + path: string; // Relative file path + matchCount: number; // Number of matching lines + preview: string; // Preview of matched content (truncated) +} + +// Detailed info for a doc file +export interface DocFileDetail { + name: string; // Display name + type: string; // MIME type or description + preview?: string; // Preview of doc content +} + +// Statistics for the query process +export interface QueryStats { + llmCalls: number; // Number of LLM API calls + searchCalls: number; // Number of search operations + toolCalls: number; // Number of tool/command calls +} + +export interface Task { + id: string; + kind: QueryKind; + query: string; + status: TaskStatus; + createdAt: number; + updatedAt: number; + etaSeconds: number; + pollCount: number; // Number of times polled + progress?: TaskProgress; // Current progress details + result?: string; + error?: string; +} + +export interface SearchResult { + relativePath: string; + matchingLines: string[]; + fullContent?: string; +} + +// ---------- Cache Types ---------- + +export interface CachedFileInfo { + uri: string; + mimeType: string; + name: string; + displayName: string; + uploadedAt: number; // Unix timestamp (ms) + expiresAt: number; // Unix timestamp (ms) + fileHash: string; // MD5 hash of file content +} + +export interface CacheData { + version: number; + files: CachedFileInfo[]; +} + +// ---------- API Usage Types ---------- + +export interface DailyUsage { + date: string; // YYYY-MM-DD + count: number; +} + +export const DAILY_LIMIT = 100; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..0efb997 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,29 @@ +// ---------- Utility Functions ---------- + +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function timestamp(): string { + return new Date().toISOString(); +} + +export function log(message: string, ...args: unknown[]): void { + console.log(`[${timestamp()}] ${message}`, ...args); +} + +export function logError(message: string, ...args: unknown[]): void { + console.error(`[${timestamp()}] ${message}`, ...args); +} + +export function logWarn(message: string, ...args: unknown[]): void { + console.warn(`[${timestamp()}] ${message}`, ...args); +} + +/** + * Truncate long text for logging, showing first and last N characters + */ +export function truncateText(text: string, headTail: number = 100): string { + if (text.length <= headTail * 2 + 10) { + return text; + } + return `${text.slice(0, headTail)}...【省略 ${text.length - headTail * 2} 字符】...${text.slice(-headTail)}`; +} diff --git a/yarn.lock b/yarn.lock index 9ac94c4..0c3bb6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,259 +2,235 @@ # yarn lockfile v1 -"@esbuild/aix-ppc64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" - integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== - -"@esbuild/android-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752" - integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== - -"@esbuild/android-arm@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a" - integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== - -"@esbuild/android-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16" - integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== - -"@esbuild/darwin-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd" - integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== - -"@esbuild/darwin-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e" - integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== - -"@esbuild/freebsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe" - integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== - -"@esbuild/freebsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3" - integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== - -"@esbuild/linux-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977" - integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== - -"@esbuild/linux-arm@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9" - integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== - -"@esbuild/linux-ia32@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0" - integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== - -"@esbuild/linux-loong64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0" - integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== - -"@esbuild/linux-mips64el@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd" - integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== - -"@esbuild/linux-ppc64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869" - integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== - -"@esbuild/linux-riscv64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6" - integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== - -"@esbuild/linux-s390x@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663" - integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== - -"@esbuild/linux-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306" - integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== - -"@esbuild/netbsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4" - integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== - -"@esbuild/netbsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076" - integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== - -"@esbuild/openbsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd" - integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== - -"@esbuild/openbsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679" - integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== - -"@esbuild/openharmony-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d" - integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== - -"@esbuild/sunos-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6" - integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== - -"@esbuild/win32-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323" - integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== - -"@esbuild/win32-ia32@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267" - integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== - -"@esbuild/win32-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5" - integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== - -"@google/generative-ai@^0.24.1": - version "0.24.1" - resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.1.tgz#634a3c06f8ea7a6125c1b0d6c1e66bb11afb52c9" - integrity sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q== - -"@modelcontextprotocol/sdk@^1.21.1": - version "1.21.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.21.1.tgz#8fba02e7581d49cc9b047aab0cfd334043321fe5" - integrity sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ== - dependencies: - 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.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" +"@esbuild/aix-ppc64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c" + integrity sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw== + +"@esbuild/android-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz#61ea550962d8aa12a9b33194394e007657a6df57" + integrity sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA== + +"@esbuild/android-arm@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz#554887821e009dd6d853f972fde6c5143f1de142" + integrity sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA== + +"@esbuild/android-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz#a7ce9d0721825fc578f9292a76d9e53334480ba2" + integrity sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A== + +"@esbuild/darwin-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz#2cb7659bd5d109803c593cfc414450d5430c8256" + integrity sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg== + +"@esbuild/darwin-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz#e741fa6b1abb0cd0364126ba34ca17fd5e7bf509" + integrity sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA== + +"@esbuild/freebsd-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz#2b64e7116865ca172d4ce034114c21f3c93e397c" + integrity sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g== + +"@esbuild/freebsd-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz#e5252551e66f499e4934efb611812f3820e990bb" + integrity sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA== + +"@esbuild/linux-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz#dc4acf235531cd6984f5d6c3b13dbfb7ddb303cb" + integrity sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw== + +"@esbuild/linux-arm@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz#56a900e39240d7d5d1d273bc053daa295c92e322" + integrity sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw== + +"@esbuild/linux-ia32@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz#d4a36d473360f6870efcd19d52bbfff59a2ed1cc" + integrity sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w== + +"@esbuild/linux-loong64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz#fcf0ab8c3eaaf45891d0195d4961cb18b579716a" + integrity sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg== + +"@esbuild/linux-mips64el@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz#598b67d34048bb7ee1901cb12e2a0a434c381c10" + integrity sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw== + +"@esbuild/linux-ppc64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz#3846c5df6b2016dab9bc95dde26c40f11e43b4c0" + integrity sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ== + +"@esbuild/linux-riscv64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz#173d4475b37c8d2c3e1707e068c174bb3f53d07d" + integrity sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA== + +"@esbuild/linux-s390x@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz#f7a4790105edcab8a5a31df26fbfac1aa3dacfab" + integrity sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w== + +"@esbuild/linux-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz#2ecc1284b1904aeb41e54c9ddc7fcd349b18f650" + integrity sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA== + +"@esbuild/netbsd-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz#e2863c2cd1501845995cb11adf26f7fe4be527b0" + integrity sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw== + +"@esbuild/netbsd-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz#93f7609e2885d1c0b5a1417885fba8d1fcc41272" + integrity sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA== + +"@esbuild/openbsd-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz#a1985604a203cdc325fd47542e106fafd698f02e" + integrity sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA== + +"@esbuild/openbsd-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz#8209e46c42f1ffbe6e4ef77a32e1f47d404ad42a" + integrity sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg== + +"@esbuild/openharmony-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz#8fade4441893d9cc44cbd7dcf3776f508ab6fb2f" + integrity sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag== + +"@esbuild/sunos-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz#980d4b9703a16f0f07016632424fc6d9a789dfc2" + integrity sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg== + +"@esbuild/win32-arm64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz#1c09a3633c949ead3d808ba37276883e71f6111a" + integrity sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg== + +"@esbuild/win32-ia32@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz#1b1e3a63ad4bef82200fef4e369e0fff7009eee5" + integrity sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ== + +"@esbuild/win32-x64@0.27.2": + version "0.27.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" + integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== + +"@google/genai@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.34.0.tgz#8a6a85c2c7eb94afbb1a999967e828cae43ee6dd" + integrity sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw== + dependencies: + google-auth-library "^10.3.0" + ws "^8.18.0" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + 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" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@types/node@^24.10.1": - version "24.10.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01" - integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ== + version "24.10.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.4.tgz#9d27c032a1b2c42a4eab8fb65c5856a8b8e098c4" + integrity sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg== dependencies: undici-types "~7.16.0" -accepts@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" - integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== - dependencies: - mime-types "^3.0.0" - negotiator "^1.0.0" - -ajv-formats@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" - integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== - dependencies: - ajv "^8.0.0" - -ajv@^8.0.0, ajv@^8.17.1: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -body-parser@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" - 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" - -bytes@3.1.2, bytes@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" +agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== -call-bound@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" - integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== - dependencies: - call-bind-apply-helpers "^1.0.2" - get-intrinsic "^1.3.0" +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -content-disposition@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" - integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== +ansi-regex@^6.0.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - safe-buffer "5.2.1" + color-convert "^2.0.1" -content-type@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== -cookie-signature@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" - integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -cookie@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" - integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +base64-js@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -cors@^2.8.5: - version "2.8.5" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== +bignumber.js@^9.0.0: + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: - object-assign "^4" - vary "^1" + balanced-match "^1.0.0" + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== -cross-spawn@^7.0.5: +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -263,211 +239,123 @@ cross-spawn@^7.0.5: shebang-command "^2.0.0" which "^2.0.1" -debug@^4.3.5, debug@^4.4.0: +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +debug@4: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" -depd@2.0.0, depd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -encodeurl@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -esbuild@~0.25.0: - version "0.25.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5" - integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.25.12" - "@esbuild/android-arm" "0.25.12" - "@esbuild/android-arm64" "0.25.12" - "@esbuild/android-x64" "0.25.12" - "@esbuild/darwin-arm64" "0.25.12" - "@esbuild/darwin-x64" "0.25.12" - "@esbuild/freebsd-arm64" "0.25.12" - "@esbuild/freebsd-x64" "0.25.12" - "@esbuild/linux-arm" "0.25.12" - "@esbuild/linux-arm64" "0.25.12" - "@esbuild/linux-ia32" "0.25.12" - "@esbuild/linux-loong64" "0.25.12" - "@esbuild/linux-mips64el" "0.25.12" - "@esbuild/linux-ppc64" "0.25.12" - "@esbuild/linux-riscv64" "0.25.12" - "@esbuild/linux-s390x" "0.25.12" - "@esbuild/linux-x64" "0.25.12" - "@esbuild/netbsd-arm64" "0.25.12" - "@esbuild/netbsd-x64" "0.25.12" - "@esbuild/openbsd-arm64" "0.25.12" - "@esbuild/openbsd-x64" "0.25.12" - "@esbuild/openharmony-arm64" "0.25.12" - "@esbuild/sunos-x64" "0.25.12" - "@esbuild/win32-arm64" "0.25.12" - "@esbuild/win32-ia32" "0.25.12" - "@esbuild/win32-x64" "0.25.12" - -escape-html@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -etag@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eventsource-parser@^3.0.0, eventsource-parser@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz#292e165e34cacbc936c3c92719ef326d4aeb4e90" - integrity sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg== - -eventsource@^3.0.2: - version "3.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" - integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== - dependencies: - eventsource-parser "^3.0.1" - -express-rate-limit@^7.5.0: - version "7.5.1" - resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.1.tgz#8c3a42f69209a3a1c969890070ece9e20a879dec" - integrity sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw== - -express@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" - 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" - -fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-uri@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" - integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== - -finalhandler@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" - 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" - -forwarded@0.2.0: +eastasianwidth@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" - integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +esbuild@~0.27.0: + version "0.27.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.2.tgz#d83ed2154d5813a5367376bb2292a9296fc83717" + integrity sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.2" + "@esbuild/android-arm" "0.27.2" + "@esbuild/android-arm64" "0.27.2" + "@esbuild/android-x64" "0.27.2" + "@esbuild/darwin-arm64" "0.27.2" + "@esbuild/darwin-x64" "0.27.2" + "@esbuild/freebsd-arm64" "0.27.2" + "@esbuild/freebsd-x64" "0.27.2" + "@esbuild/linux-arm" "0.27.2" + "@esbuild/linux-arm64" "0.27.2" + "@esbuild/linux-ia32" "0.27.2" + "@esbuild/linux-loong64" "0.27.2" + "@esbuild/linux-mips64el" "0.27.2" + "@esbuild/linux-ppc64" "0.27.2" + "@esbuild/linux-riscv64" "0.27.2" + "@esbuild/linux-s390x" "0.27.2" + "@esbuild/linux-x64" "0.27.2" + "@esbuild/netbsd-arm64" "0.27.2" + "@esbuild/netbsd-x64" "0.27.2" + "@esbuild/openbsd-arm64" "0.27.2" + "@esbuild/openbsd-x64" "0.27.2" + "@esbuild/openharmony-arm64" "0.27.2" + "@esbuild/sunos-x64" "0.27.2" + "@esbuild/win32-arm64" "0.27.2" + "@esbuild/win32-ia32" "0.27.2" + "@esbuild/win32-x64" "0.27.2" + +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - 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" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== +gaxios@^7.0.0: + version "7.1.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-7.1.3.tgz#c5312f4254abc1b8ab53aef30c22c5229b80b1e1" + integrity sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + node-fetch "^3.3.2" + rimraf "^5.0.1" + +gcp-metadata@^8.0.0: + version "8.1.2" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz#e62e3373ddf41fc727ccc31c55c687b798bee898" + integrity sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg== dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" + gaxios "^7.0.0" + google-logging-utils "^1.0.0" + json-bigint "^1.0.0" get-tsconfig@^4.7.5: version "4.13.0" @@ -476,247 +364,171 @@ get-tsconfig@^4.7.5: dependencies: resolve-pkg-maps "^1.0.0" -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -http-errors@2.0.0, http-errors@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - 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" - -iconv-lite@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.0.tgz#c50cd80e6746ca8115eb98743afa81aa0e147a3e" - integrity sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== +glob@^10.3.7: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + 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" + +google-auth-library@^10.3.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-10.5.0.tgz#3f0ebd47173496b91d2868f572bb8a8180c4b561" + integrity sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^7.0.0" + gcp-metadata "^8.0.0" + google-logging-utils "^1.0.0" + gtoken "^8.0.0" + jws "^4.0.0" + +google-logging-utils@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz#17b71f1f95d266d2ddd356b8f00178433f041b17" + integrity sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA== + +gtoken@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-8.0.0.tgz#d67a0e346dd441bfb54ad14040ddc3b632886575" + integrity sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw== + dependencies: + gaxios "^7.0.0" + jws "^4.0.0" + +https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" + agent-base "^7.1.2" + debug "4" -inherits@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-promise@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" - integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" -media-typer@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" - integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" -merge-descriptors@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" - integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== +jws@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" -mime-db@^1.54.0: - version "1.54.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" - integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== -mime-types@^3.0.0, mime-types@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" - integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: - mime-db "^1.54.0" + brace-expansion "^2.0.1" + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -negotiator@^1.0.0: +node-domexception@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" - integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -object-assign@^4: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.3: - version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" - integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== - -on-finished@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== dependencies: - ee-first "1.1.1" + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" -once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -parseurl@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-to-regexp@^8.0.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" - integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== - -pkce-challenge@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz#c3a405cb49e272094a38e890a2b51da0228c4d97" - integrity sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ== - -proxy-addr@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -qs@^6.14.0: - version "6.14.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" - integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - side-channel "^1.1.0" + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -range-parser@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.1.tgz#ced5cd79a77bbb0496d707f2a0f9e1ae3aecdcb1" - integrity sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.7.0" - unpipe "1.0.0" - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +prettier@^3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f" + integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== resolve-pkg-maps@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -router@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" - integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== +rimraf@^5.0.1: + version "5.0.10" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" + integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== dependencies: - debug "^4.4.0" - depd "^2.0.0" - is-promise "^4.0.0" - parseurl "^1.3.3" - path-to-regexp "^8.0.0" + glob "^10.3.7" -safe-buffer@5.2.1: +safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -send@^1.1.0, send@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" - 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" - -serve-static@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" - 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" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -729,80 +541,69 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" -side-channel-map@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" - integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" - 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" - -side-channel@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" - 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" - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" -statuses@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" - integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" tsx@^4.20.6: - version "4.20.6" - resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.20.6.tgz#8fb803fd9c1f70e8ccc93b5d7c5e03c3979ccb2e" - integrity sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg== + version "4.21.0" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.21.0.tgz#32aa6cf17481e336f756195e6fe04dae3e6308b1" + integrity sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw== dependencies: - esbuild "~0.25.0" + esbuild "~0.27.0" get-tsconfig "^4.7.5" optionalDependencies: fsevents "~2.3.3" -type-is@^2.0.0, type-is@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" - integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== - dependencies: - content-type "^1.0.5" - media-typer "^1.1.0" - mime-types "^3.0.0" - typescript@^5.9.3: version "5.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" @@ -813,15 +614,15 @@ undici-types@~7.16.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +undici@^7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-7.16.0.tgz#cb2a1e957726d458b536e3f076bf51f066901c1a" + integrity sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g== -vary@^1, vary@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== which@^2.0.1: version "2.0.2" @@ -830,17 +631,30 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" -zod-to-json-schema@^3.24.1: - version "3.24.6" - resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d" - integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg== +ws@^8.18.0: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== -zod@^3.23.8, zod@^3.25.76: +zod@^3.25.76: version "3.25.76" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==