diff --git a/README.md b/README.md index 286aa4c..a032b18 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ๐Ÿš€ DeepSeekAI - Smart Web Assistant +# DeepSeekAI - Smart Web Assistant + Regent Agent Orchestrator
@@ -12,167 +12,561 @@
-## ๐Ÿ“– Introduction +## Introduction -DeepSeekAI is an unofficial, open-source browser extension that lets you summon a private DeepSeek-powered co-pilot anywhere on the web. Highlight text, tap a quick action, or press a shortcut to open a floating chat workspace that streams answers, shows reasoning traces, and remembers your preferred layout. The project is independent from DeepSeek, and you must provide your own API key (DeepSeek or any OpenAI-compatible endpoint). +DeepSeekAI is an open-source browser extension that lets you summon a private AI co-pilot anywhere on the web. Highlight text, tap a quick action, or press a shortcut to open a floating chat workspace that streams answers, shows reasoning traces, and remembers your preferred layout. -> **Note**: This extension is a community project and is not affiliated with DeepSeek. Keys, custom endpoints, and preferences are stored only in `chrome.storage.sync` on your device. +**Regent Mothership** is the optional self-hosted backend that adds cross-session memory, multi-user workspaces, autonomous AI agents with MCP tool access, and real-time collaboration via WebSocket. -### ๐Ÿ”Œ Supported API Providers +> **Note**: This extension is a community project and is not affiliated with DeepSeek. Keys, custom endpoints, and preferences are stored only in `chrome.storage.sync` on your device. The Mothership backend is entirely self-hosted โ€” your data never leaves your infrastructure. + +### Supported API Providers - [DeepSeek](https://deepseek.com) (official endpoint) -- [ByteDance Volcengine](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=OXTHJAF8) -- [SiliconFlow](https://cloud.siliconflow.cn/i/lStn36vH) +- [ByteDance Volcengine](https://www.volcengine.com/experience/ark) +- [SiliconFlow](https://cloud.siliconflow.cn) - [OpenRouter](https://openrouter.ai/models) -- [AiHubMix](https://aihubmix.com?aff=SmJB) +- [AiHubMix](https://aihubmix.com) - [Tencent Cloud](https://cloud.tencent.com/document/product/1772/115969) - [IFlytek Star](https://training.xfyun.cn/modelService) - [Baidu Cloud](https://console.bce.baidu.com/qianfan/modelcenter/model/buildIn/list) - [Aliyun](https://bailian.console.aliyun.com/#/model-market) -- Unlimited self-hosted/custom providers that expose an OpenAI-compatible `/chat/completions` endpoint - -## โœจ Feature Overview - -### ๐Ÿช„ Inline Assistants -- Rich quick-action bubble appears beside any text selection with Chat, Copy, Translate (19 languages), Explain, Summarize, Email, and Analyze templates. -- SelectionPreservationManager keeps the DOM range alive so the bubble never steals your highlight during double/triple clicks or context-menu usage. -- Right-click context menu entry and toolbar popup both reuse the same flow, so highlighted text, manual prompts, and keyboard shortcuts share the session logic. - -### ๐ŸชŸ Floating Workspace -- `interactjs` gives the chat window magnetic drag + resize handles, snap animations, and a persistent minimize icon whose position is saved per user. -- Toggle "Remember window size" to keep the workspace dimensions across sites, and "Pin window" to prevent accidental closes when clicking outside. -- Minimized state, popup visibility, and icon location are tracked by `popupStateManager`, ensuring state survives selection changes. -- Built-in input container includes auto-expanding textarea, send icon, abort/stop square, and smart focus rules so existing form inputs retain priority. -- Each answer provides inline copy + regenerate controls; DeepSeek-R1/openrouter reasoning is rendered above the final response with a collapsible panel. -- Auto-scroll follows the stream until you scroll manually. Scroll momentum + cooldown logic prevent janky jumps. - -### ๐Ÿง  Provider & Model Controls -- Popup UI (English/Chinese) manages API keys per provider, preferred language (auto-detect or force output), and whether the selection bubble is enabled. -- Add, rename, hide, or delete custom providers with their own base URL, display name, default model, and placeholder API-key links. -- ModelManager stores multiple custom models per provider. Dropdowns support inline delete buttons, and forms auto-save via TempStateManager so unfinished entries survive popup reloads. -- Configure a global custom system prompt used for every conversation, or override per quick action via templated prompts. - -### ๐Ÿ“ Rendering, Safety & UX Polishing -- Markdown-It + highlight.js + KaTeX + DOMPurify ensure rich formatting, syntax highlighting, math rendering, and sanitized HTML. -- Code blocks gain reusable โ€œCopyโ€ controls, while each AI block also exposes regenerate + share-ready text copy actions. -- Streaming is proxied through the background service worker using modern `fetch` + `AbortController`, so stop/regenerate/shortcut commands instantly cut network traffic. -- ThemeManager listens to `prefers-color-scheme` and toggles CSS variables to keep the popover and quick buttons readable in both modes. - -### โŒจ๏ธ Shortcuts & Invocation Options -- Two Chrome commands ship by default: - - `Ctrl/Cmd + Shift + Y` โ†’ toggle chat (new session) - - `Ctrl/Cmd + Shift + U` โ†’ show/hide chat (preserve session) -- Use `chrome://extensions/shortcuts` (or the โ€œShortcut Settingsโ€ link inside the popup) to rebind commands. -- Context menu entry (โ€œDeepSeek AIโ€) sends the selected text directly, and the icon in the toolbar opens the configuration popup. - -### ๐Ÿ” Privacy & Onboarding -- On first install we open [`src/Instructions/Instructions.html`](src/Instructions/instructions.html), an offline-friendly Apple-style guide covering every screen. -- `PRIVACY.html` documents exactly what is stored (API keys + user preferences in local browser storage) and reminds you that no remote server is involved. -- DOMPurify sanitizes all rendered HTML, and no telemetry or analytics is collected. - -## ๐Ÿ”„ How It Works +- Any self-hosted/custom provider exposing an OpenAI-compatible `/chat/completions` endpoint + +--- + +## Architecture Overview + +```mermaid +graph TB + subgraph "Browser Extension" + CS[Content Script
Selection, Quick Actions, Popup] + BG[Background Service Worker
API Proxy, WS Client, Commands] + PP[Popup Settings UI
Providers, Keys, Mothership Config] + end + + subgraph "Mothership Backend (Self-Hosted)" + API[Hono REST API
/api/v1/*] + WS[WebSocket Gateway
First-Message Auth, Heartbeat] + RT[Agent Runtime
LLM Streaming, Tool Loop] + MCP[MCP Pool
stdio + HTTP Transports] + MEM[Memory System
FTS5 + sqlite-vec Hybrid Search] + DB[(SQLite + WAL
better-sqlite3)] + BUS[EventBus
In-Process Pub/Sub] + end + + subgraph "External Services" + LLM[LLM Provider
DeepSeek / OpenRouter / etc.] + TOOL[MCP Servers
Filesystem, GitHub, Postgres, etc.] + end + + CS <-->|chrome.runtime.sendMessage| BG + PP -->|chrome.storage| BG + BG <-->|HTTP REST| API + BG <-->|WebSocket| WS + WS <--> BUS + API --> DB + WS --> DB + RT -->|SSE Stream| LLM + RT -->|JSON-RPC 2.0| MCP + MCP -->|stdio / HTTP| TOOL + RT <--> BUS + MEM --> DB + RT --> MEM +``` + +### Component Responsibilities + +| Component | Role | +|---|---| +| **Content Script** | Selection tracking, quick-action bubble, floating workspace, markdown rendering, theme | +| **Background Worker** | Network proxy (fetch + AbortController), WS connection manager, keyboard commands | +| **Popup UI** | Provider/model config, API keys, Mothership connection, language, system prompt | +| **REST API** | Auth (register/login/revoke), workspaces, sessions, events, memory, agents, MCP, invites | +| **WS Gateway** | Real-time bidirectional messaging with first-message auth, heartbeat, event broadcasting | +| **Agent Runtime** | Mastra-powered agentic loop (max 10 steps): LLM stream โ†’ tool calls โ†’ MCP execution โ†’ repeat | +| **MCP Pool** | Manages MCP server connections (stdio subprocess or HTTP), command allowlist, SSRF protection | +| **Memory System** | Hybrid search: FTS5 full-text + sqlite-vec cosine similarity, merged via Reciprocal Rank Fusion | +| **EventBus** | In-process EventEmitter routing agent streams, tool calls, events, and notifications to WS connections | + +--- + +## Sequence Diagrams + +### Extension Chat Flow (Standalone, No Mothership) ```mermaid sequenceDiagram participant User participant Content as Content Script - participant Background - participant Provider + participant Background as Service Worker + participant Provider as LLM Provider + User->>Content: Select text / press shortcut - Content->>Background: chrome.runtime.sendMessage({ action: "proxyRequest" | "getSettings" }) - Background->>Provider: fetch(OpenAI-compatible endpoint) - Provider-->>Background: SSE / JSON chunks - Background-->>Content: streamResponse events (AbortController aware) - Content-->>User: Renders markdown, reasoning, quick actions + Content->>Background: sendMessage({ action: "proxyRequest", url, headers, body }) + Background->>Provider: fetch(OpenAI-compatible /chat/completions) + Provider-->>Background: SSE stream (data: {...}\n\n) + + loop Each SSE Chunk + Background-->>Content: sendMessage({ type: "streamResponse", data }) + Content-->>User: Renders markdown incrementally + end + + Note over Background,Provider: AbortController allows instant cancellation +``` + +### Mothership Connection & Event Flow + +```mermaid +sequenceDiagram + participant Ext as Extension (background.js) + participant GW as WS Gateway + participant Bus as EventBus + participant DB as SQLite + + Ext->>GW: WebSocket connect (no token in URL) + GW-->>Ext: Connection accepted + + Ext->>GW: { type: "auth", payload: { token, tabId } } + GW->>GW: verifyToken() + iat revocation check + GW-->>Ext: { type: "connected", payload: { userId, username } } + + Ext->>GW: { type: "tab:register", payload: { workspaceId } } + GW->>GW: Bind connection to workspace + + Ext->>GW: { type: "provider:credentials", payload: { provider, apiKey } } + GW->>DB: Upsert encrypted credentials + GW-->>Ext: { type: "provider:ack" } + + Ext->>GW: { type: "events:store", payload: { sessionId, events: [...] } } + GW->>DB: Transaction: upsert session + insert events + GW->>Bus: emit("events:new") + Bus-->>GW: Forward to other tabs in same workspace + GW-->>Ext: { type: "events:cross", payload: { sessionId, events } } +``` + +### Agent Execution Flow (Mastra-Powered) + +```mermaid +sequenceDiagram + participant Client as Extension / API Client + participant GW as WS Gateway + participant Ctrl as Agent Control Handler + participant RT as Agent Runtime (Orchestration Shell) + participant MA as Mastra Agent + participant LLM as LLM Provider + participant MCP as MCP Server (via Bridge) + participant Bus as EventBus + + Client->>GW: { type: "agent:start", payload: { agentId, input } } + GW->>Ctrl: handleAgentStart(conn, payload) + Ctrl->>RT: startRun({ agent, userId, input }) + RT->>RT: INSERT agent_runs (status='running') + RT-->>Ctrl: runId + Ctrl-->>Client: { type: "agent:started", payload: { runId } } + Ctrl->>Bus: Subscribe to agent:stream, agent:tool_call, agent:error + + RT->>RT: Fetch memory context (hybridSearch) + RT->>RT: Bridge MCP tools โ†’ Mastra createTool() + RT->>MA: new Agent({ model, tools, instructions }) + RT->>MA: agent.stream(input, { maxSteps: 10, abortSignal }) + + loop Mastra Agentic Loop (max 10 steps) + MA->>LLM: Stream request (provider-native format) + loop Text Chunks + LLM-->>MA: text-delta events + MA-->>RT: textStream chunks + RT->>Bus: emit("agent:stream", { chunk }) + Bus-->>Client: { type: "agent:stream", payload: { chunk, done: false } } + end + + alt Tool Calls Present + MA->>MCP: execute bridged tool (via our security layer) + MCP-->>MA: tool result (truncated to 4KB) + MA->>MA: Append tool result, continue loop + RT->>Bus: emit("agent:tool_call", { tool, input, output }) + Bus-->>Client: { type: "agent:tool_call", payload: { tool, output } } + else No Tool Calls + MA->>MA: Complete + end + end + + RT->>RT: UPDATE agent_runs (status='completed', output, duration_ms) + RT->>Bus: emit("agent:stream", { done: true, result }) + Bus-->>Client: { type: "agent:stream", payload: { done: true, result } } ``` -- `content/content.js` glues together selection tracking, quick actions, the popup workspace, markdown renderer, theme watcher, scroll manager, and focus manager. -- `background.js` is the single network surface: it loads provider settings, streams responses, parses errors, handles aborts, manages commands/context menus, and opens onboarding tabs. -- `popup/` houses the modular settings UI (ApiKeyManager, ProviderManager, ModelManager, SystemPromptManager, etc.) with i18n + autosave. -- `Instructions/` exposes the offline guide viewed after installation. +### Memory Hybrid Search (RAG) -## ๐Ÿš€ Installation & Build +```mermaid +sequenceDiagram + participant Caller as Agent Runtime / API + participant Search as Hybrid Search + participant FTS as FTS5 Engine + participant Vec as sqlite-vec + participant Embed as Embedding Provider + + Caller->>Search: hybridSearch(userId, wsId, query, limit) + + par Full-Text Search + Search->>FTS: MATCH sanitized query + BM25 rank + FTS-->>Search: FTS results with rank scores + and Vector Search + Search->>Embed: embed(userId, query) + Embed-->>Search: float[384] vector + Search->>Vec: vec_distance_cosine(embedding, queryVec) + Vec-->>Search: Vector results sorted by distance + end + + Search->>Search: Reciprocal Rank Fusion (k=60) + Search-->>Caller: Merged results sorted by combined score +``` + +--- + +## Feature Overview + +### Inline Assistants +- Rich quick-action bubble beside any text selection: Chat, Copy, Translate (19 languages), Explain, Summarize, Email, Analyze. +- SelectionPreservationManager keeps the DOM range alive so the bubble never steals your highlight. +- Right-click context menu and toolbar popup share the same session logic. + +### Floating Workspace +- Drag + resize via `interactjs` with snap animations and persistent minimize icon position. +- Toggle "Remember window size" and "Pin window" for cross-site consistency. +- Auto-expanding textarea, send/abort controls, copy + regenerate per answer, collapsible reasoning blocks. +- Auto-scroll follows the stream until manual scroll, with momentum + cooldown logic. + +### Regent Agents (Mothership) +- **Custom Agents**: Define agents with name, system prompt, and MCP server bindings. +- **Agentic Tool Loop**: Up to 10 rounds of LLM reasoning + MCP tool execution per run. +- **Memory-Augmented (RAG)**: Agents automatically fetch relevant context from past sessions via hybrid FTS5 + vector search. +- **Real-Time Streaming**: Response chunks, tool calls, and errors stream to the UI via WebSocket. +- **MCP Integration**: Connect external tools (filesystem, GitHub, Postgres, custom servers) via Model Context Protocol (stdio or HTTP transport). +- **RBAC**: Owner > Admin > Member > Viewer role hierarchy per workspace. + +### Provider & Model Controls +- Popup UI manages API keys per provider, language preference, selection bubble toggle. +- Add/rename/delete custom providers with base URL, default model, and placeholder links. +- Global custom system prompt, overridable per quick action. + +### Privacy & Security +- Extension: API keys stored only in `chrome.storage.sync`. No telemetry. DOMPurify sanitizes all rendered HTML. +- Mothership: JWT auth with token revocation, AES-256-GCM encryption for API keys at rest, RBAC, rate limiting, SSRF protection, MCP command allowlist, non-root Docker container. + +--- + +## Project Layout -### 1. Install from the store (recommended) +``` +. +โ”œโ”€โ”€ src/ # Browser extension source +โ”‚ โ”œโ”€โ”€ manifest.json # MV3 metadata & permissions +โ”‚ โ”œโ”€โ”€ background.js # Service worker: API proxy, WS client, commands +โ”‚ โ”œโ”€โ”€ content/ # Content script layer +โ”‚ โ”‚ โ”œโ”€โ”€ content.js # Main orchestrator +โ”‚ โ”‚ โ”œโ”€โ”€ components/ # SelectionManager, PopupManager, InputContainer, etc. +โ”‚ โ”‚ โ”œโ”€โ”€ services/ # apiService (background proxy) +โ”‚ โ”‚ โ”œโ”€โ”€ utils/ # markdownRenderer, themeManager, scrollManager, etc. +โ”‚ โ”‚ โ”œโ”€โ”€ handlers/ # MouseHandler +โ”‚ โ”‚ โ”œโ”€โ”€ regent/ # RegentOrchestrator, Sidecar, Detector, Sidebar, AI Service +โ”‚ โ”‚ โ””โ”€โ”€ styles/ # Extension CSS +โ”‚ โ”œโ”€โ”€ popup/ # Settings UI (managers, i18n, HTML) +โ”‚ โ””โ”€โ”€ Instructions/ # Onboarding guide +โ”‚ +โ”œโ”€โ”€ mothership/ # Self-hosted backend +โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Server entry: Hono + WS + graceful shutdown +โ”‚ โ”‚ โ”œโ”€โ”€ config.ts # Env var parsing (PORT, JWT_SECRET, DB_PATH, LOG_LEVEL) +โ”‚ โ”‚ โ”œโ”€โ”€ api/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Route mounting, CORS config +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ middleware/ # auth.ts (JWT), workspace.ts (RBAC) +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ routes/ # auth, workspaces, sessions, events, memory, +โ”‚ โ”‚ โ”‚ # agents, mcp, invites, notifications +โ”‚ โ”‚ โ”œโ”€โ”€ ws/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ gateway.ts # WS upgrade, first-message auth, message dispatch +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ registry.ts # Connection registry, workspace broadcast +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ handlers/ # tabRegister, eventsStore, contextQuery, agentControl +โ”‚ โ”‚ โ”œโ”€โ”€ agents/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ manager.ts # Agent CRUD (create, list, get, delete) +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ runtime.ts # Orchestration shell + Mastra Agent execution +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ mcpBridge.ts # MCP tools โ†’ Mastra createTool() bridge +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ modelResolver.ts # DB credentials โ†’ Mastra model config +โ”‚ โ”‚ โ”œโ”€โ”€ mcp/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ client.ts # JSON-RPC 2.0 MCP client (stdio + HTTP) +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ pool.ts # Connection pool, workspace tool aggregation +โ”‚ โ”‚ โ”œโ”€โ”€ memory/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ embeddings.ts # Vector embedding + provider credential management +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ search.ts # Hybrid search: FTS5 + sqlite-vec + RRF merge +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ retention.ts # Data retention scheduler +โ”‚ โ”‚ โ”œโ”€โ”€ events/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ bus.ts # In-process EventEmitter pub/sub +โ”‚ โ”‚ โ”œโ”€โ”€ queue/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ laneQueue.ts # Per-session serial execution queue +โ”‚ โ”‚ โ”œโ”€โ”€ db/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # SQLite init, atomic migrations, WAL mode +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schema.ts # TypeScript type definitions +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ migrations/ # 001_foundation โ†’ 004_multiuser +โ”‚ โ”‚ โ””โ”€โ”€ utils/ # crypto, id, logger, rateLimit +โ”‚ โ”œโ”€โ”€ Dockerfile # Multi-stage Node 22, non-root user +โ”‚ โ”œโ”€โ”€ docker-compose.yml # mothership + Caddy reverse proxy +โ”‚ โ”œโ”€โ”€ Caddyfile # Auto-HTTPS reverse proxy config +โ”‚ โ””โ”€โ”€ .env.example # Environment variable template +โ”‚ +โ”œโ”€โ”€ webpack.config.js # Extension bundler config +โ”œโ”€โ”€ PRIVACY.html # Privacy policy +โ””โ”€โ”€ README.md +``` + +--- + +## Installation & Setup + +### 1. Extension (Chrome / Edge) + +**From the store (recommended):** - **Chrome**: [Chrome Web Store](https://chromewebstore.google.com/detail/bjjobdlpgglckcmhgmmecijpfobmcpap) -- **Microsoft Edge**: enable โ€œAllow extensions from other stores,โ€ then install via the same Chrome Web Store listing above. +- **Edge**: Enable "Allow extensions from other stores" then install via Chrome Web Store. -### 2. Manual installation / development flow +**Manual / development build:** ```bash -# Requirements: Node.js 18+, pnpm (or npm), and a Chromium-based browser +# Requirements: Node.js 18+, pnpm (or npm) pnpm install -pnpm run build # outputs the production bundle into dist/ -``` - -1. Open `chrome://extensions` โ†’ enable **Developer mode** โ†’ **Load unpacked** โ†’ pick the `dist` folder. -2. To ship a store package, run one of: - - `pnpm run build:zip` โ†’ `extension.zip` - - `pnpm run build:chrome` โ†’ `chrome-submission.zip` - - `pnpm run build:edge` โ†’ `edge-submission.zip` -3. Upload the generated ZIP to the respective store dashboards. - -## ๐Ÿงฉ Setup & Daily Use -1. Click the extension icon to open the popup. -2. Choose a provider (or create a custom one with a name + base URL + default model) and paste its API key. Each provider keeps its own key and optional custom API URL. -3. Pick or add a model. Non-DeepSeek providers require an explicit model ID; the UI will auto-prompt you to add one if missing. -4. Configure behavior: - - Enable/disable the selection quick-action bubble. - - Choose automatic language detection or force a language from the dropdown (20+ locales). - - Toggle **Save Window Size**, **Pin Window**, and **Custom System Prompt**. - - Use the **Shortcut Settings** link to jump to Chromeโ€™s command editor. -5. Highlight text (or open the chat via shortcut) โ†’ the quick-action bubble appears โ†’ select Chat or a template. You can also open the floating window first and paste custom prompts. -6. While streaming, use the stop square icon to abort. Each answer ends with copy + regenerate icons; reasoning blocks collapse/expand with one click. -7. Need a refresher? Open the in-extension [usage guide](src/Instructions/instructions.html) or switch to the Simplified Chinese README linked at the top. - -## โŒจ๏ธ Shortcuts & Quick Actions -- **Quick actions:** - - `Chat` โ†’ sends selection verbatim. - - `Copy` โ†’ copies selection without opening chat. - - `Translate` โ†’ language picker drives a prompt that asks DeepSeek to translate into your chosen target language. - - `Explain`, `Summarize`, `Email`, `Analyze` โ†’ curated prompts (with MBTI-flavored tone) for instant structured answers. -- **Window commands:** `toggle-chat` destroys and recreates the session; `show-hide-chat` keeps the current context alive between invocations; `close-chat` is exposed internally for context menu cleanup. -- **Context menu:** right-click โ†’ โ€œDeepSeek AIโ€ to push highlighted text directly into a new chat with a contextual greeting (morning/afternoon/evening). - -## ๐Ÿ—๏ธ Project Layout & Stack +pnpm run build # outputs to dist/ ``` -. -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ manifest.json # MV3 metadata & permissions -โ”‚ โ”œโ”€โ”€ background.js # service worker + proxy + commands -โ”‚ โ”œโ”€โ”€ content/ # selection bubble, popup workspace, services, utils, styles -โ”‚ โ”œโ”€โ”€ popup/ # settings UI (managers, i18n, HTML) -โ”‚ โ””โ”€โ”€ Instructions/ # onboarding guide (HTML + JS) -โ”œโ”€โ”€ dist/ # build output (loaded during development/packaging) -โ”œโ”€โ”€ extension.zip # generated via build:zip / build:chrome / build:edge -โ”œโ”€โ”€ webpack.config.js # bundler config (Babel, CSS loader, copy plugin, terser) -โ”œโ”€โ”€ PRIVACY.html # privacy policy -โ””โ”€โ”€ README*.md # documentation (English + ็ฎ€ไฝ“ไธญๆ–‡) + +1. Open `chrome://extensions` โ†’ enable **Developer mode** โ†’ **Load unpacked** โ†’ select `dist/`. +2. Click the extension icon โ†’ configure your API provider and key. +3. Highlight text or press `Ctrl+Shift+Y` to start chatting. + +### 2. Mothership Backend (Optional) + +The Mothership enables cross-session memory, agents, workspaces, and real-time sync. The extension works fully standalone without it. + +#### Option A: Docker (Recommended for Production) + +```bash +cd mothership + +# 1. Create your environment file +cp .env.example .env +# Edit .env โ€” set JWT_SECRET to a long random string: +# JWT_SECRET=$(openssl rand -hex 32) + +# 2. Start the stack (mothership + Caddy reverse proxy) +docker compose up -d + +# 3. Verify it's running +curl http://localhost:3001/api/v1/health +# โ†’ {"status":"ok","ts":...} +``` + +**Environment variables:** + +| Variable | Default | Description | +|---|---|---| +| `JWT_SECRET` | **(required)** | Secret for signing JWT tokens. Use `openssl rand -hex 32` | +| `PORT` | `3001` | HTTP server port | +| `DB_PATH` | `./data/mothership.db` | SQLite database file path | +| `LOG_LEVEL` | `info` | Pino log level: `debug`, `info`, `warn`, `error` | +| `EXTENSION_ID` | *(optional)* | Pin CORS to a specific Chrome extension ID | +| `ALLOWED_ORIGINS` | *(optional)* | Comma-separated allowed CORS origins | + +#### Option B: Local Development (No Docker) + +```bash +cd mothership + +# 1. Install dependencies +npm install + +# 2. Run in dev mode (auto-reload on file changes) +npm run dev +# โ†’ Mothership listening on http://localhost:3001 +# โ†’ [WARN] No JWT_SECRET set โ€” using random ephemeral secret + +# Or build and run production: +npm run build +JWT_SECRET=your-secret-here npm start +``` + +**TypeScript commands:** +```bash +npm run typecheck # Type validation only (no emit) +npm run build # Compile TypeScript โ†’ dist/ +npm start # Run compiled dist/index.js +npm run dev # Watch mode via tsx +``` + +### 3. Connect Extension to Mothership + +Once the backend is running: + +1. **Register a user** (first-time only): + ```bash + curl -X POST http://localhost:3001/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"username":"yourname","password":"your-password-here"}' + # โ†’ {"id":"...","username":"yourname","token":"eyJ...","workspaceId":"..."} + ``` + +2. **Or generate an API token** (if already registered): + ```bash + curl -X POST http://localhost:3001/api/v1/auth/token/generate \ + -H 'Content-Type: application/json' \ + -d '{"username":"yourname","password":"your-password-here"}' + # โ†’ {"token":"eyJ...","expiresIn":"30d"} + ``` + +3. **Configure the extension:** + - Click the extension icon โ†’ scroll to **Mothership** section. + - **URL**: `http://localhost:3001` (or your public domain). + - **Token**: Paste the JWT token from step 1 or 2. + - Click **Connect**. The status dot turns green. + +4. **What happens on connect:** + - The extension opens a WebSocket to `ws://localhost:3001/ws`. + - Authenticates via first-message auth (token never in URL). + - Registers with the workspace and forwards provider credentials. + - Session events stream to the backend for memory storage. + - Cross-tab events sync in real-time. + +--- + +## Agent System + +Agents are custom AI assistants that run server-side with access to MCP tools and workspace memory. + +### Agent Runtime (Powered by Mastra) + +The agent runtime uses **[Mastra](https://mastra.ai)** (`@mastra/core`) as the inner execution engine, wrapped in our orchestration shell for DB persistence, bus events, safety limits, and MCP security: + +``` +โ”Œโ”€ Orchestration Shell (our code) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ startRun() โ†’ DB record, AbortController โ”‚ +โ”‚ cancelRun() โ†’ abort, update DB โ”‚ +โ”‚ finishRun() โ†’ write output/status/duration to DB โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€ Mastra Agent (inner engine) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Agent({ model, tools, instructions }) โ”‚ โ”‚ +โ”‚ โ”‚ .stream(input, { maxSteps, abortSignal }) โ”‚ โ”‚ +โ”‚ โ”‚ textStream โ†’ bus.emit('agent:stream') โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€ MCP Security Layer (unchanged) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Command allowlist, SSRF protection, path traversal โ”‚ โ”‚ +โ”‚ โ”‚ โ†’ Bridged to Mastra tools via createTool() โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +- **Mastra handles**: LLM streaming, SSE parsing, tool call assembly, provider abstraction (800+ models / 47 providers), thinking/reasoning support. +- **We handle**: DB persistence, EventBus โ†’ WebSocket streaming, MCP security (command allowlist, SSRF, path traversal), safety limits (output caps, tool result truncation), run lifecycle tracking. +- **MCP Bridge**: Our `mcpBridge.ts` wraps MCP tools as Mastra `createTool()` instances. The MCP security layer (`mcp/client.ts`) remains unchanged. +- **Model Resolver**: `modelResolver.ts` maps encrypted DB credentials to Mastra `OpenAICompatibleConfig` objects, supporting DeepSeek, OpenRouter, SiliconFlow, OpenAI, and any custom endpoint. +- **Memory-augmented context (RAG)**: Before each run, hybrid search (FTS5 + vector cosine + RRF) injects relevant past context into the agent's instructions. + +### Runtime Limits + +| Limit | Value | +|---|---| +| Max tool rounds | 10 | +| Max output size | 256 KB | +| Max tool result size | 4 KB | +| Per-round timeout | 2 min | +| Agent runs rate limit | 5/min per user | + +### API Endpoints + +| Method | Endpoint | Auth | Description | +|---|---|---|---| +| `GET` | `/workspaces/:wsId/agents` | viewer+ | List agents | +| `POST` | `/workspaces/:wsId/agents` | admin+ | Create agent | +| `DELETE` | `/workspaces/:wsId/agents/:id` | admin+ | Delete agent | +| `POST` | `/workspaces/:wsId/agents/:id/run` | member+ | Start a run | +| `GET` | `/workspaces/:wsId/agents/:id/runs` | viewer+ | List runs | +| `GET` | `/workspaces/:wsId/agents/:id/runs/:rid` | viewer+ | Get run details | +| `POST` | `/workspaces/:wsId/agents/:id/runs/:rid/cancel` | member+ | Cancel a run | + +### WebSocket Messages + +**Client โ†’ Server:** +```json +{ "type": "agent:start", "payload": { "agentId": "...", "input": "Research X", "sessionId": "..." } } +{ "type": "agent:stop", "payload": { "runId": "..." } } +``` + +**Server โ†’ Client:** +```json +{ "type": "agent:started", "payload": { "runId": "...", "agentId": "..." } } +{ "type": "agent:stream", "payload": { "runId": "...", "chunk": "text", "done": false } } +{ "type": "agent:tool_call", "payload": { "runId": "...", "tool": "name", "input": {}, "output": "..." } } +{ "type": "agent:stream", "payload": { "runId": "...", "chunk": "", "done": true, "result": "..." } } +{ "type": "agent:error", "payload": { "runId": "...", "error": "message" } } +``` + +### MCP Server Setup + +Connect external tools via Model Context Protocol: + +```bash +# Example: connect a filesystem MCP server +curl -X POST http://localhost:3001/api/v1/workspaces/WS_ID/mcp \ + -H 'Authorization: Bearer TOKEN' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "filesystem", + "transport": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"] + }' ``` -**Key dependencies:** `interactjs`, `markdown-it`, `highlight.js`, `DOMPurify`, `katex`, `clipboard`, `perfect-scrollbar`, and `openai` (for payload typing) plus the MV3 APIs exposed by Chrome/Edge. +**Allowed commands** (security allowlist): `npx`, `node`, `python`, `python3`, `uvx`, `docker`, and `mcp-server-*` prefixed binaries. Absolute paths, dangerous interpreter flags (`-e`, `--eval`, `-c`), and private network URLs are blocked. + +--- + +## Database Schema -## ๐Ÿ”’ Privacy & Security -- API keys, preferences, quick-action states, and minimized icon positions live only inside `chrome.storage.sync`. -- Text is sent solely to the provider endpoint you configure. There are no intermediary servers, analytics calls, or remote logs. -- The offline [privacy policy](PRIVACY.html) inside the repo details data handling, and DOMPurify removes any potentially unsafe markup before rendering. +Four migrations build the schema incrementally: -## ๐Ÿค Contributing -Contributions are welcomeโ€”bug reports, documentation fixes, and feature proposals all help the community. +| Migration | Tables | +|---|---| +| `001_foundation` | `users`, `sessions`, `events` | +| `002_memory_fts` | `memory_entries` (with embedding blob), `memory_fts` (FTS5 virtual table) | +| `003_agents_mcp` | `agents`, `agent_runs`, `mcp_servers` | +| `004_multiuser` | `workspaces`, `workspace_members`, `invites`, `notifications`, provider credentials on `users` | + +SQLite runs in WAL mode with `busy_timeout=5000` and `foreign_keys=ON`. The `sqlite-vec` extension enables cosine similarity search on embedding vectors. + +--- + +## Shortcuts & Quick Actions +- **Quick actions:** Chat, Copy, Translate (19 languages), Explain, Summarize, Email, Analyze. +- **Keyboard shortcuts:** + - `Ctrl/Cmd + Shift + Y` โ†’ Toggle chat (new session) + - `Ctrl/Cmd + Shift + U` โ†’ Show/hide chat (preserve session) +- **Context menu:** Right-click โ†’ "DeepSeek AI" to chat with selected text. +- Rebind via `chrome://extensions/shortcuts`. + +## Contributing + +Contributions are welcome โ€” bug reports, documentation fixes, and feature proposals all help. 1. Fork the repo and create a branch (`git checkout -b feature/my-update`). 2. Install deps + build once (`pnpm install && pnpm run build`). -3. Make your changes, keep them focused, and run `pnpm run build` again to ensure `dist/` refreshes. -4. Submit a Pull Request describing the change, affected files, and any verification notes. +3. For Mothership changes: `cd mothership && npm install && npm run typecheck`. +4. Submit a Pull Request describing the change and verification notes. -## ๐Ÿ“„ License +## License This project is licensed under the MIT License - see [LICENSE](LICENSE) for details. -## ๐Ÿ“ฎ Contact +## Contact - Issues: [GitHub Issues](https://github.com/DeepLifeStudio/DeepSeekAI/issues) - Email: [1024jianghu@gmail.com](mailto:1024jianghu@gmail.com) - Twitter/X: [@DeepLifeStudio](https://x.com/DeepLifeStudio)
-

If this project helps you, please consider giving it a โญ๏ธ

-
\ No newline at end of file +

If this project helps you, please consider giving it a star

+ diff --git a/mothership/.dockerignore b/mothership/.dockerignore new file mode 100644 index 0000000..9fb7dd2 --- /dev/null +++ b/mothership/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +.gitignore +*.md +.env* +data/ diff --git a/mothership/.env.example b/mothership/.env.example new file mode 100644 index 0000000..d6ffcdf --- /dev/null +++ b/mothership/.env.example @@ -0,0 +1,4 @@ +PORT=3001 +JWT_SECRET=change-me-to-a-random-string +DB_PATH=./data/mothership.db +LOG_LEVEL=info diff --git a/mothership/.gitignore b/mothership/.gitignore new file mode 100644 index 0000000..c0d536a --- /dev/null +++ b/mothership/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +data/*.db +data/*.db-wal +data/*.db-shm +.env diff --git a/mothership/Caddyfile b/mothership/Caddyfile new file mode 100644 index 0000000..b59f9fc --- /dev/null +++ b/mothership/Caddyfile @@ -0,0 +1,3 @@ +{$DOMAIN:localhost} { + reverse_proxy mothership:3001 +} diff --git a/mothership/Dockerfile b/mothership/Dockerfile new file mode 100644 index 0000000..7da8dcf --- /dev/null +++ b/mothership/Dockerfile @@ -0,0 +1,28 @@ +# Multi-stage build for Regent Mothership + +FROM node:22-slim AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src/ ./src/ +RUN npx tsc + +FROM node:22-slim +RUN addgroup --system mothership && adduser --system --ingroup mothership mothership +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev && chown -R mothership:mothership /app +COPY --from=build --chown=mothership:mothership /app/dist/ ./dist/ +COPY --chown=mothership:mothership src/db/migrations/ ./dist/db/migrations/ + +ENV PORT=3001 +ENV DB_PATH=/data/mothership.db +ENV LOG_LEVEL=info +VOLUME /data + +RUN mkdir -p /data && chown mothership:mothership /data + +USER mothership +EXPOSE 3001 +CMD ["node", "dist/index.js"] diff --git a/mothership/data/.gitkeep b/mothership/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/mothership/docker-compose.yml b/mothership/docker-compose.yml new file mode 100644 index 0000000..e170976 --- /dev/null +++ b/mothership/docker-compose.yml @@ -0,0 +1,37 @@ +services: + mothership: + build: . + ports: + - "3001:3001" + volumes: + - mothership-data:/data + environment: + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required} + - NODE_ENV=production + - DB_PATH=/data/mothership.db + - LOG_LEVEL=info + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3001/api/v1/health').then(r=>process.exit(r.ok?0:1))"] + interval: 30s + timeout: 5s + retries: 3 + restart: unless-stopped + + caddy: + image: caddy:2-alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - caddy-data:/data + - caddy-config:/config + depends_on: + mothership: + condition: service_healthy + restart: unless-stopped + +volumes: + mothership-data: + caddy-data: + caddy-config: diff --git a/mothership/package-lock.json b/mothership/package-lock.json new file mode 100644 index 0000000..e424515 --- /dev/null +++ b/mothership/package-lock.json @@ -0,0 +1,3645 @@ +{ + "name": "regent-mothership", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "regent-mothership", + "version": "0.1.0", + "dependencies": { + "@hono/node-server": "^1.13.7", + "@mastra/core": "^1.8.0", + "better-sqlite3": "^11.7.0", + "hono": "^4.6.0", + "jose": "^5.9.0", + "nanoid": "^5.0.9", + "pino": "^9.5.0", + "sqlite-vec": "^0.1.7-alpha.2", + "ws": "^8.18.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@a2a-js/sdk": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.2.5.tgz", + "integrity": "sha512-VTDuRS5V0ATbJ/LkaQlisMnTAeYKXAK6scMguVBstf+KIBQ7HIuKhiXLv+G/hvejkV+THoXzoNifInAkU81P1g==", + "dependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.23", + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "express": "^4.21.2", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.1.tgz", + "integrity": "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", + "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/provider-utils-v5": { + "name": "@ai-sdk/provider-utils", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.20.tgz", + "integrity": "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.1", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider-utils-v6": { + "name": "@ai-sdk/provider-utils", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.0.tgz", + "integrity": "sha512-HyCyOls9I3a3e38+gtvOJOEjuw9KRcvbBnCL5GBuSmJvS9Jh9v3fz7pRC6ha1EUo/ZH1zwvLWYXBMtic8MTguA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.0", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider-utils-v6/node_modules/@ai-sdk/provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.0.tgz", + "integrity": "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils/node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/@ai-sdk/provider-v5": { + "name": "@ai-sdk/provider", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-v6": { + "name": "@ai-sdk/provider", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.0.tgz", + "integrity": "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/ui-utils-v5": { + "name": "@ai-sdk/ui-utils", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz", + "integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/ui-utils-v5/node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-2.1.4.tgz", + "integrity": "sha512-7kMz0BJpMvgAMkyglums7B2vtrn5g0a0am77JY0GjkZZNetOBCFn7AG7gKCwT0QPiXyxW7YIQSgtARknUEOcxQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@lukeed/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mastra/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@mastra/core/-/core-1.8.0.tgz", + "integrity": "sha512-AK6Isj21mWlwX1zIZNUxgAQvRfjJmdjsPsKoh1cOvaM+h748S4U48TJ5DsmundSj/8NBeKHmYXqH2RYqwN35nw==", + "license": "Apache-2.0", + "dependencies": { + "@a2a-js/sdk": "~0.2.4", + "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.20", + "@ai-sdk/provider-utils-v6": "npm:@ai-sdk/provider-utils@4.0.0", + "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", + "@ai-sdk/provider-v6": "npm:@ai-sdk/provider@3.0.0", + "@ai-sdk/ui-utils-v5": "npm:@ai-sdk/ui-utils@1.2.11", + "@isaacs/ttlcache": "^2.1.4", + "@lukeed/uuid": "^2.0.1", + "@mastra/schema-compat": "1.1.3", + "@modelcontextprotocol/sdk": "^1.17.5", + "@sindresorhus/slugify": "^2.2.1", + "dotenv": "^17.2.3", + "gray-matter": "^4.0.3", + "hono": "^4.11.9", + "hono-openapi": "^1.1.1", + "js-tiktoken": "^1.0.21", + "json-schema": "^0.4.0", + "lru-cache": "^11.2.6", + "p-map": "^7.0.3", + "p-retry": "^7.1.0", + "picomatch": "^4.0.3", + "radash": "^12.1.1", + "xxhash-wasm": "^1.1.0" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@mastra/schema-compat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@mastra/schema-compat/-/schema-compat-1.1.3.tgz", + "integrity": "sha512-szLMJhqfnEn4VctFLKRZ2NIpfg+3UTghQWgy8Fcdchj2HvHxB2uilJxRybM9ugMmvyE+W48tVdz4Xi2Z1P3pFA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema-to-zod": "^2.7.0", + "zod-from-json-schema": "^0.5.0", + "zod-from-json-schema-v3": "npm:zod-from-json-schema@^0.0.5", + "zod-to-json-schema": "^3.24.6" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@standard-community/standard-json": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@standard-community/standard-json/-/standard-json-0.3.5.tgz", + "integrity": "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/json-schema": "^7.0.15", + "@valibot/to-json-schema": "^1.3.0", + "arktype": "^2.1.20", + "effect": "^3.16.8", + "quansync": "^0.2.11", + "sury": "^10.0.0", + "typebox": "^1.0.17", + "valibot": "^1.1.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.24.5" + }, + "peerDependenciesMeta": { + "@valibot/to-json-schema": { + "optional": true + }, + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@standard-community/standard-openapi": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@standard-community/standard-openapi/-/standard-openapi-0.2.9.tgz", + "integrity": "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@standard-community/standard-json": "^0.3.5", + "@standard-schema/spec": "^1.0.0", + "arktype": "^2.1.20", + "effect": "^3.17.14", + "openapi-types": "^12.1.3", + "sury": "^10.0.0", + "typebox": "^1.0.0", + "valibot": "^1.1.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-openapi": "^4" + }, + "peerDependenciesMeta": { + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-openapi": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hono-openapi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hono-openapi/-/hono-openapi-1.2.0.tgz", + "integrity": "sha512-t3u4v8YCltExDl4d9cLqg/mcrYFSs9Gjb5puF1CePPrvv1JQOo1Kc50HAmGt47CWHIoc/W8Q9LY3t3yqU0dxFw==", + "license": "MIT", + "peerDependencies": { + "@hono/standard-validator": "^0.2.0", + "@standard-community/standard-json": "^0.3.5", + "@standard-community/standard-openapi": "^0.2.9", + "@types/json-schema": "^7.0.15", + "hono": "^4.8.3", + "openapi-types": "^12.1.3" + }, + "peerDependenciesMeta": { + "@hono/standard-validator": { + "optional": true + }, + "hono": { + "optional": true + } + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-to-zod": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/json-schema-to-zod/-/json-schema-to-zod-2.7.0.tgz", + "integrity": "sha512-eW59l3NQ6sa3HcB+Ahf7pP6iGU7MY4we5JsPqXQ2ZcIPF8QxSg/lkY8lN0Js/AG0NjMbk+nZGUfHlceiHF+bwQ==", + "license": "ISC", + "bin": { + "json-schema-to-zod": "dist/cjs/cli.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/radash": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/radash/-/radash-12.1.1.tgz", + "integrity": "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==", + "license": "MIT", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/sqlite-vec": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.7-alpha.2.tgz", + "integrity": "sha512-rNgRCv+4V4Ed3yc33Qr+nNmjhtrMnnHzXfLVPeGb28Dx5mmDL3Ngw/Wk8vhCGjj76+oC6gnkmMG8y73BZWGBwQ==", + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.7-alpha.2", + "sqlite-vec-darwin-x64": "0.1.7-alpha.2", + "sqlite-vec-linux-arm64": "0.1.7-alpha.2", + "sqlite-vec-linux-x64": "0.1.7-alpha.2", + "sqlite-vec-windows-x64": "0.1.7-alpha.2" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.7-alpha.2.tgz", + "integrity": "sha512-raIATOqFYkeCHhb/t3r7W7Cf2lVYdf4J3ogJ6GFc8PQEgHCPEsi+bYnm2JT84MzLfTlSTIdxr4/NKv+zF7oLPw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-jeZEELsQjjRsVojsvU5iKxOvkaVuE+JYC8Y4Ma8U45aAERrDYmqZoHvgSG7cg1PXL3bMlumFTAmHynf1y4pOzA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.7-alpha.2.tgz", + "integrity": "sha512-6Spj4Nfi7tG13jsUG+W7jnT0bCTWbyPImu2M8nWp20fNrd1SZ4g3CSlDAK8GBdavX7wRlbBHCZ+BDa++rbDewA==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-IcgrbHaDccTVhXDf8Orwdc2+hgDLAFORl6OBUhcvlmwswwBP1hqBTSEhovClG4NItwTOBNgpwOoQ7Qp3VDPWLg==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-TRP6hTjAcwvQ6xpCZvjP00pdlda8J38ArFy1lMYhtQWXiIBmWnhMaMbq4kaeCYwvTTddfidatRS+TJrwIKB/oQ==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-from-json-schema": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/zod-from-json-schema/-/zod-from-json-schema-0.5.2.tgz", + "integrity": "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g==", + "license": "MIT", + "dependencies": { + "zod": "^4.0.17" + } + }, + "node_modules/zod-from-json-schema-v3": { + "name": "zod-from-json-schema", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/zod-from-json-schema/-/zod-from-json-schema-0.0.5.tgz", + "integrity": "sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ==", + "license": "MIT", + "dependencies": { + "zod": "^3.24.2" + } + }, + "node_modules/zod-from-json-schema/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/mothership/package.json b/mothership/package.json new file mode 100644 index 0000000..658b675 --- /dev/null +++ b/mothership/package.json @@ -0,0 +1,31 @@ +{ + "name": "regent-mothership", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^1.13.7", + "@mastra/core": "^1.8.0", + "better-sqlite3": "^11.7.0", + "hono": "^4.6.0", + "jose": "^5.9.0", + "nanoid": "^5.0.9", + "pino": "^9.5.0", + "sqlite-vec": "^0.1.7-alpha.2", + "ws": "^8.18.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.0", + "@types/ws": "^8.5.13", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/mothership/src/agents/manager.ts b/mothership/src/agents/manager.ts new file mode 100644 index 0000000..6b03607 --- /dev/null +++ b/mothership/src/agents/manager.ts @@ -0,0 +1,36 @@ +/** + * Agent Manager โ€” CRUD + lifecycle for workspace agents. + */ + +import { getDb } from '../db/index.js'; +import { newId } from '../utils/id.js'; +import type { Agent } from '../db/schema.js'; + +export function createAgent(workspaceId: string, opts: { name: string; systemPrompt?: string; mcpServers?: string[] }): Agent { + const db = getDb(); + const id = newId(); + const mcpServers = opts.mcpServers?.length ? JSON.stringify(opts.mcpServers) : null; + + db.prepare(`INSERT INTO agents (id, workspace_id, name, system_prompt, mcp_servers) + VALUES (?, ?, ?, ?, ?)`) + .run(id, workspaceId, opts.name, opts.systemPrompt ?? null, mcpServers); + + return db.prepare('SELECT * FROM agents WHERE id = ?').get(id) as Agent; +} + +export function listAgents(workspaceId: string): Agent[] { + const db = getDb(); + return db.prepare('SELECT * FROM agents WHERE workspace_id = ? ORDER BY created_at DESC') + .all(workspaceId) as Agent[]; +} + +export function getAgent(id: string): Agent | null { + const db = getDb(); + return db.prepare('SELECT * FROM agents WHERE id = ?').get(id) as Agent | null; +} + +export function deleteAgent(id: string, workspaceId: string): boolean { + const db = getDb(); + const result = db.prepare('DELETE FROM agents WHERE id = ? AND workspace_id = ?').run(id, workspaceId); + return result.changes > 0; +} diff --git a/mothership/src/agents/mcpBridge.ts b/mothership/src/agents/mcpBridge.ts new file mode 100644 index 0000000..a62d93f --- /dev/null +++ b/mothership/src/agents/mcpBridge.ts @@ -0,0 +1,42 @@ +/** + * MCP โ†’ Mastra Tool Bridge โ€” wraps our secure MCP tools as Mastra createTool() instances. + * Our MCP security layer (command allowlist, SSRF protection, path traversal blocking) + * remains unchanged โ€” this bridge just adapts the interface. + */ + +import { createTool } from '@mastra/core/tools'; +import { z } from 'zod'; +import { callTool } from '../mcp/pool.js'; +import { log } from '../utils/logger.js'; +import type { McpTool } from '../mcp/client.js'; + +const MAX_TOOL_RESULT_SIZE = 4096; + +/** Bridge a single MCP tool to a Mastra-compatible createTool() instance */ +export function mcpToMastraTool(tool: McpTool & { serverId: string }) { + return createTool({ + id: tool.name, + description: tool.description || tool.name, + inputSchema: z.record(z.string(), z.any()), + execute: async (params) => { + const result = await callTool(tool.serverId, tool.name, params as Record); + const text = result.content?.map((c: { text?: string }) => c.text || '').join('\n') || 'Success'; + return text.length > MAX_TOOL_RESULT_SIZE + ? text.slice(0, MAX_TOOL_RESULT_SIZE) + '\n[truncated]' + : text; + }, + }); +} + +/** Bridge all workspace MCP tools to Mastra format */ +export function bridgeWorkspaceTools(mcpTools: Array) { + const tools: Record> = {}; + for (const t of mcpTools) { + try { + tools[t.name] = mcpToMastraTool(t); + } catch (err) { + log.warn({ err, tool: t.name }, 'Failed to bridge MCP tool'); + } + } + return tools; +} diff --git a/mothership/src/agents/modelResolver.ts b/mothership/src/agents/modelResolver.ts new file mode 100644 index 0000000..f36ea23 --- /dev/null +++ b/mothership/src/agents/modelResolver.ts @@ -0,0 +1,31 @@ +/** + * Model Resolver โ€” maps our encrypted DB credentials to Mastra model configs. + * Uses OpenAICompatibleConfig so any provider with an OpenAI-compatible + * /chat/completions endpoint works out of the box. + */ + +import type { OpenAICompatibleConfig } from '@mastra/core/llm'; +import type { ProviderCredential } from '../db/schema.js'; + +const PROVIDER_DEFAULTS: Record = { + deepseek: { url: 'https://api.deepseek.com/v1', model: 'deepseek-chat' }, + openrouter: { url: 'https://openrouter.ai/api/v1', model: 'anthropic/claude-3.5-sonnet' }, + siliconflow: { url: 'https://api.siliconflow.cn/v1', model: 'deepseek-ai/DeepSeek-V3' }, + openai: { url: 'https://api.openai.com/v1', model: 'gpt-4o' }, +}; + +/** + * Resolve provider credentials โ†’ Mastra OpenAICompatibleConfig. + * Mastra routes this through its OpenAI-compatible provider automatically. + */ +export function resolveModel(creds: ProviderCredential): OpenAICompatibleConfig { + const defaults = PROVIDER_DEFAULTS[creds.provider] ?? {}; + const baseUrl = (creds.api_url ?? defaults.url ?? '').replace(/\/+$/, ''); + const model = creds.model ?? defaults.model ?? 'deepseek-chat'; + + return { + id: `${creds.provider}/${model}` as `${string}/${string}`, + url: baseUrl || undefined, + apiKey: creds.api_key, + }; +} diff --git a/mothership/src/agents/runtime.ts b/mothership/src/agents/runtime.ts new file mode 100644 index 0000000..c6900c9 --- /dev/null +++ b/mothership/src/agents/runtime.ts @@ -0,0 +1,180 @@ +/** + * Agent Runtime โ€” execute agent tasks with LLM + MCP tool access. + * Uses Mastra Agent as the inner execution engine for LLM streaming + tool calling. + * Our orchestration shell (DB writes, bus events, safety limits, run tracking) wraps it. + */ + +import { Agent as MastraAgent } from '@mastra/core/agent'; +import { getDb } from '../db/index.js'; +import { newId } from '../utils/id.js'; +import { bus } from '../events/bus.js'; +import { getProviderCredentials } from '../memory/embeddings.js'; +import { getWorkspaceTools } from '../mcp/pool.js'; +import { hybridSearch } from '../memory/search.js'; +import { log } from '../utils/logger.js'; +import { resolveModel } from './modelResolver.js'; +import { bridgeWorkspaceTools } from './mcpBridge.js'; +import type { Agent, AgentRun } from '../db/schema.js'; + +const MAX_TOOL_ROUNDS = 10; +const MAX_OUTPUT_SIZE = 256 * 1024; // 256KB + +/** Active runs: stores both controller and startTime */ +const activeRuns = new Map(); + +/** Runs already finalized โ€” prevents double finishRun */ +const finishedRuns = new Set(); + +export interface RunOptions { + agent: Agent; + userId: string; + input: string; + sessionId?: string; +} + +/** Start an agent run โ€” streams chunks via bus, returns run ID */ +export async function startRun(opts: RunOptions): Promise { + const { agent, userId, input, sessionId } = opts; + const db = getDb(); + const runId = newId(); + const startTime = Date.now(); + + db.prepare(`INSERT INTO agent_runs (id, agent_id, workspace_id, user_id, input, session_id, status) + VALUES (?, ?, ?, ?, ?, ?, 'running')`) + .run(runId, agent.id, agent.workspace_id, userId, input, sessionId ?? null); + + const controller = new AbortController(); + activeRuns.set(runId, { controller, startTime }); + + executeRun(runId, opts, controller.signal, startTime).catch((err) => { + log.warn({ err, runId }, 'Agent run failed'); + finishRun(runId, null, 'failed', startTime); + }); + + return runId; +} + +/** Cancel a running agent */ +export function cancelRun(runId: string): boolean { + const entry = activeRuns.get(runId); + if (!entry) return false; + entry.controller.abort(); + activeRuns.delete(runId); + finishRun(runId, null, 'cancelled', entry.startTime); + return true; +} + +/** Get run status */ +export function getRun(runId: string): AgentRun | null { + const db = getDb(); + return db.prepare('SELECT * FROM agent_runs WHERE id = ?').get(runId) as AgentRun | null; +} + +/** List runs for an agent */ +export function listRuns(agentId: string, limit = 20): AgentRun[] { + const db = getDb(); + return db.prepare('SELECT * FROM agent_runs WHERE agent_id = ? ORDER BY started_at DESC LIMIT ?') + .all(agentId, limit) as AgentRun[]; +} + +// โ”€โ”€ Internal execution (powered by Mastra Agent) โ”€โ”€ + +async function executeRun(runId: string, opts: RunOptions, signal: AbortSignal, startTime: number) { + const { agent, userId, input } = opts; + + const creds = getProviderCredentials(userId); + if (!creds) { + bus.emit('agent:error', { runId, workspaceId: agent.workspace_id, error: 'No provider credentials configured' }); + finishRun(runId, null, 'failed', startTime); + return; + } + + // Gather RAG context from memory + let contextBlock = ''; + try { + const memories = await hybridSearch(userId, agent.workspace_id, input, 5); + if (memories.length) { + contextBlock = '\n\nRelevant context from past sessions:\n' + + memories.map(m => `- ${m.entry.content}`).join('\n'); + } + } catch (err) { + log.debug({ err, runId }, 'Memory search failed, continuing without context'); + } + + // Bridge MCP tools through our security layer โ†’ Mastra tools + const mcpTools = getWorkspaceTools(agent.workspace_id); + const tools = bridgeWorkspaceTools(mcpTools); + + // Create Mastra agent for this run + const mastraAgent = new MastraAgent({ + id: `run-${runId}`, + name: agent.name || `Agent ${agent.id}`, + instructions: (agent.system_prompt || 'You are a helpful agent.') + contextBlock, + model: resolveModel(creds), + tools, + }); + + let fullOutput = ''; + + try { + const result = await mastraAgent.stream(input, { + maxSteps: MAX_TOOL_ROUNDS, + abortSignal: signal, + }); + + // Stream text chunks โ†’ bus + const reader = result.textStream.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + fullOutput += value; + bus.emit('agent:stream', { runId, workspaceId: agent.workspace_id, chunk: value, done: false }); + + if (fullOutput.length > MAX_OUTPUT_SIZE) { + fullOutput = fullOutput.slice(0, MAX_OUTPUT_SIZE) + '\n[output truncated]'; + break; + } + } + } finally { + reader.releaseLock(); + } + + // Emit tool call events (resolved after stream completes) + try { + const toolResults = await result.toolResults; + for (const tr of toolResults) { + bus.emit('agent:tool_call', { + runId, workspaceId: agent.workspace_id, + tool: tr.payload.toolName, input: tr.payload.args, output: tr.payload.result, + }); + } + } catch (err) { + log.debug({ err, runId }, 'Tool results extraction failed'); + } + } catch (err: any) { + if (signal.aborted) return; // Cancelled โ€” finishRun already called by cancelRun + bus.emit('agent:error', { runId, workspaceId: agent.workspace_id, error: err.message || String(err) }); + finishRun(runId, fullOutput || null, 'failed', startTime); + return; + } + + activeRuns.delete(runId); + finishRun(runId, fullOutput, 'completed', startTime); + bus.emit('agent:stream', { runId, workspaceId: agent.workspace_id, chunk: '', done: true, result: fullOutput }); +} + +/** Update run record in DB โ€” guarded against double invocation */ +function finishRun(runId: string, output: string | null, status: AgentRun['status'], startTime: number) { + if (finishedRuns.has(runId)) return; + finishedRuns.add(runId); + // Evict from set after 60s to prevent unbounded growth + setTimeout(() => finishedRuns.delete(runId), 60_000); + + const db = getDb(); + const duration = startTime ? Date.now() - startTime : null; + db.prepare(`UPDATE agent_runs SET output = ?, status = ?, finished_at = datetime('now'), duration_ms = ? WHERE id = ? AND status = 'running'`) + .run(output, status, duration, runId); + activeRuns.delete(runId); +} diff --git a/mothership/src/api/index.ts b/mothership/src/api/index.ts new file mode 100644 index 0000000..e659326 --- /dev/null +++ b/mothership/src/api/index.ts @@ -0,0 +1,47 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { authRoutes } from './routes/auth.js'; +import { workspaceRoutes } from './routes/workspaces.js'; +import { sessionRoutes } from './routes/sessions.js'; +import { eventRoutes } from './routes/events.js'; +import { memoryRoutes } from './routes/memory.js'; +import { agentRoutes } from './routes/agents.js'; +import { mcpRoutes } from './routes/mcp.js'; +import { inviteRoutes, redeemInvite } from './routes/invites.js'; +import { notificationRoutes } from './routes/notifications.js'; +import { authMiddleware } from './middleware/auth.js'; + +export const api = new Hono().basePath('/api/v1'); + +// CORS: pin to specific extension ID or allowed origins โ€” no wildcard +api.use('*', cors({ + origin: (origin) => { + // Pin to specific extension ID if configured, otherwise allow any chrome-extension + const extId = process.env.EXTENSION_ID; + if (extId && origin === `chrome-extension://${extId}`) return origin; + if (!extId && origin?.startsWith('chrome-extension://')) return origin; + // Allow configured origins + const allowed = process.env.ALLOWED_ORIGINS?.split(',').map(s => s.trim()) || []; + if (origin && allowed.includes(origin)) return origin; + // Same-origin requests (no Origin header) โ€” return empty to allow but not expose wildcard + if (!origin) return ''; + return null as any; + }, +})); + +// Health check +api.get('/health', (c) => c.json({ status: 'ok', ts: Date.now() })); + +// Mount routes +api.route('/auth', authRoutes); +api.route('/workspaces', workspaceRoutes); +api.route('/workspaces/:wsId/sessions', sessionRoutes); +api.route('/workspaces/:wsId/events', eventRoutes); +api.route('/workspaces/:wsId/memory', memoryRoutes); +api.route('/workspaces/:wsId/agents', agentRoutes); +api.route('/workspaces/:wsId/mcp', mcpRoutes); +api.route('/workspaces/:wsId/invites', inviteRoutes); +api.route('/notifications', notificationRoutes); + +// Invite redemption (under auth) +api.post('/auth/pair', authMiddleware, redeemInvite); diff --git a/mothership/src/api/middleware/auth.ts b/mothership/src/api/middleware/auth.ts new file mode 100644 index 0000000..6655642 --- /dev/null +++ b/mothership/src/api/middleware/auth.ts @@ -0,0 +1,43 @@ +import { createMiddleware } from 'hono/factory'; +import { verifyToken } from '../../utils/crypto.js'; +import { getDb } from '../../db/index.js'; + +export type AuthPayload = { userId: string; username: string }; + +// Augment Hono's context variables +declare module 'hono' { + interface ContextVariableMap { + auth: AuthPayload; + } +} + +export const authMiddleware = createMiddleware(async (c, next) => { + const header = c.req.header('Authorization'); + if (!header?.startsWith('Bearer ')) { + return c.json({ error: 'Missing or invalid Authorization header' }, 401); + } + + try { + const payload = await verifyToken(header.slice(7)); + + // Validate payload types before trusting + if (typeof payload.sub !== 'string' || typeof payload.username !== 'string') { + return c.json({ error: 'Malformed token payload' }, 401); + } + const userId = payload.sub; + + // Token revocation check: token's iat must be after user's updated_at + if (typeof payload.iat === 'number') { + const db = getDb(); + const user = db.prepare('SELECT updated_at FROM users WHERE id = ?').get(userId) as { updated_at: string } | undefined; + if (user && payload.iat * 1000 < new Date(user.updated_at).getTime()) { + return c.json({ error: 'Token has been revoked' }, 401); + } + } + + c.set('auth', { userId, username: payload.username }); + await next(); + } catch { + return c.json({ error: 'Invalid or expired token' }, 401); + } +}); diff --git a/mothership/src/api/middleware/workspace.ts b/mothership/src/api/middleware/workspace.ts new file mode 100644 index 0000000..36e37e5 --- /dev/null +++ b/mothership/src/api/middleware/workspace.ts @@ -0,0 +1,40 @@ +/** + * Shared workspace membership verification with role-based access control. + * Replaces 6 duplicate verifyMembership functions across route files. + */ + +import { getDb } from '../../db/index.js'; + +const ROLE_HIERARCHY: Record = { owner: 40, admin: 30, member: 20, viewer: 10 }; + +export interface WorkspaceContext { + wsId: string; + userId: string; + role: string; + db: ReturnType; +} + +/** + * Verify the authenticated user is a member of the workspace. + * Optionally enforce a minimum role level. + * + * @param c - Hono context (uses `c.req.param('wsId')` and `c.get('auth')`) + * @param minRole - Minimum role required (default: 'viewer' โ€” any member) + * @returns WorkspaceContext or null if unauthorized + */ +export function verifyMembership(c: any, minRole: string = 'viewer'): WorkspaceContext | null { + const wsId = c.req.param('wsId'); + const { userId } = c.get('auth'); + const db = getDb(); + + const member = db.prepare('SELECT role FROM workspace_members WHERE workspace_id = ? AND user_id = ?') + .get(wsId, userId) as { role: string } | undefined; + if (!member) return null; + + // Check role hierarchy: owner > admin > member > viewer + const userLevel = ROLE_HIERARCHY[member.role] ?? 0; + const requiredLevel = ROLE_HIERARCHY[minRole] ?? 0; + if (userLevel < requiredLevel) return null; + + return { wsId, userId, role: member.role, db }; +} diff --git a/mothership/src/api/routes/agents.ts b/mothership/src/api/routes/agents.ts new file mode 100644 index 0000000..e6220ea --- /dev/null +++ b/mothership/src/api/routes/agents.ts @@ -0,0 +1,101 @@ +/** + * Agent REST API โ€” CRUD + run management. + */ + +import { Hono } from 'hono'; +import { authMiddleware } from '../middleware/auth.js'; +import { verifyMembership } from '../middleware/workspace.js'; +import { createAgent, listAgents, getAgent, deleteAgent } from '../../agents/manager.js'; +import { startRun, cancelRun, getRun, listRuns } from '../../agents/runtime.js'; +import { checkRateLimit } from '../../utils/rateLimit.js'; + +export const agentRoutes = new Hono(); +agentRoutes.use('*', authMiddleware); + +/** GET /workspaces/:wsId/agents โ€” viewer+ */ +agentRoutes.get('/', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + return c.json(listAgents(ctx.wsId)); +}); + +/** POST /workspaces/:wsId/agents โ€” admin+ */ +agentRoutes.post('/', async (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const { name, systemPrompt, mcpServers } = await c.req.json<{ + name: string; systemPrompt?: string; mcpServers?: string[]; + }>(); + if (!name || name.length > 256) return c.json({ error: 'name required (max 256 chars)' }, 400); + if (systemPrompt && systemPrompt.length > 16384) return c.json({ error: 'systemPrompt too long (max 16KB)' }, 400); + + const agent = createAgent(ctx.wsId, { name, systemPrompt, mcpServers }); + return c.json(agent, 201); +}); + +/** DELETE /workspaces/:wsId/agents/:id โ€” admin+ */ +agentRoutes.delete('/:id', (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const ok = deleteAgent(c.req.param('id'), ctx.wsId); + if (!ok) return c.json({ error: 'Not found' }, 404); + return c.json({ ok: true }); +}); + +/** POST /workspaces/:wsId/agents/:id/run โ€” member+ */ +agentRoutes.post('/:id/run', async (c) => { + const ctx = verifyMembership(c, 'member'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const agent = getAgent(c.req.param('id')); + if (!agent || agent.workspace_id !== ctx.wsId) return c.json({ error: 'Agent not found' }, 404); + + const { input, sessionId } = await c.req.json<{ input: string; sessionId?: string }>(); + if (!input || input.length > 32768) return c.json({ error: 'input required (max 32KB)' }, 400); + + if (!checkRateLimit(ctx.userId, 'agent_runs', 5)) { + return c.json({ error: 'Rate limit exceeded' }, 429); + } + + const runId = await startRun({ agent, userId: ctx.userId, input, sessionId }); + return c.json({ runId, status: 'running' }, 201); +}); + +/** GET /workspaces/:wsId/agents/:id/runs โ€” viewer+ */ +agentRoutes.get('/:id/runs', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const agent = getAgent(c.req.param('id')); + if (!agent || agent.workspace_id !== ctx.wsId) return c.json({ error: 'Agent not found' }, 404); + + const limit = Math.min(parseInt(c.req.query('limit') || '20', 10) || 20, 100); + return c.json(listRuns(agent.id, limit)); +}); + +/** GET /workspaces/:wsId/agents/:id/runs/:rid โ€” viewer+ */ +agentRoutes.get('/:id/runs/:rid', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const run = getRun(c.req.param('rid')); + if (!run || run.workspace_id !== ctx.wsId) return c.json({ error: 'Run not found' }, 404); + return c.json(run); +}); + +/** POST /workspaces/:wsId/agents/:id/runs/:rid/cancel โ€” member+ */ +agentRoutes.post('/:id/runs/:rid/cancel', (c) => { + const ctx = verifyMembership(c, 'member'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const run = getRun(c.req.param('rid')); + if (!run || run.agent_id !== c.req.param('id') || run.workspace_id !== ctx.wsId) { + return c.json({ error: 'Run not found' }, 404); + } + + const ok = cancelRun(c.req.param('rid')); + if (!ok) return c.json({ error: 'Run already finished' }, 404); + return c.json({ ok: true }); +}); diff --git a/mothership/src/api/routes/auth.ts b/mothership/src/api/routes/auth.ts new file mode 100644 index 0000000..7cd7fba --- /dev/null +++ b/mothership/src/api/routes/auth.ts @@ -0,0 +1,106 @@ +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { hashPassword, verifyPassword, signToken } from '../../utils/crypto.js'; +import { checkRateLimit } from '../../utils/rateLimit.js'; +import { authMiddleware } from '../middleware/auth.js'; +import type { User } from '../../db/schema.js'; + +export const authRoutes = new Hono(); + +const USERNAME_RE = /^[a-zA-Z0-9_\-]{3,64}$/; + +/** Shared credential verification โ€” returns User or null */ +function authenticate(username: string, password: string): User | null { + if (!username || !password) return null; + const db = getDb(); + const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined; + if (!user || !verifyPassword(password, user.password_hash)) return null; + return user; +} + +// POST /auth/register โ€” rate limited +authRoutes.post('/register', async (c) => { + const { username, password } = await c.req.json<{ username: string; password: string }>(); + + if (!username || !USERNAME_RE.test(username)) { + return c.json({ error: 'Username must be 3-64 alphanumeric/underscore/hyphen characters' }, 400); + } + if (!password || password.length < 8) { + return c.json({ error: 'Password min 8 chars' }, 400); + } + + // Rate limit by IP (use 'register' bucket, cost 10 per registration) + const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'; + if (!checkRateLimit(`ip:${ip}`, 'auth', 10)) { + return c.json({ error: 'Too many requests, try again later' }, 429); + } + + const db = getDb(); + const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username); + if (existing) return c.json({ error: 'Username taken' }, 409); + + const id = newId(); + const wsId = newId(); + + // Atomic: create user + default workspace + membership in one transaction + db.transaction(() => { + db.prepare('INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)').run( + id, username, hashPassword(password) + ); + db.prepare('INSERT INTO workspaces (id, name, owner_id) VALUES (?, ?, ?)').run( + wsId, `${username}'s workspace`, id + ); + db.prepare('INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)').run( + wsId, id, 'owner' + ); + })(); + + const token = await signToken({ sub: id, username }); + return c.json({ id, username, token, workspaceId: wsId }, 201); +}); + +// POST /auth/login โ€” rate limited +authRoutes.post('/login', async (c) => { + const { username, password } = await c.req.json<{ username: string; password: string }>(); + + const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'; + if (!checkRateLimit(`ip:${ip}`, 'auth', 3)) { + return c.json({ error: 'Too many login attempts, try again later' }, 429); + } + + const user = authenticate(username, password); + if (!user) return c.json({ error: 'Invalid credentials' }, 401); + + const token = await signToken({ sub: user.id, username: user.username }); + return c.json({ id: user.id, username: user.username, token }); +}); + +// POST /auth/token/generate โ€” extension API token (30d, refreshable) +authRoutes.post('/token/generate', async (c) => { + const { username, password } = await c.req.json<{ username: string; password: string }>(); + + const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'; + if (!checkRateLimit(`ip:${ip}`, 'auth', 3)) { + return c.json({ error: 'Too many requests' }, 429); + } + + const user = authenticate(username, password); + if (!user) return c.json({ error: 'Invalid credentials' }, 401); + + const token = await signToken({ sub: user.id, username: user.username }, '30d'); + + // Update token_issued_after for revocation support + const db = getDb(); + db.prepare("UPDATE users SET updated_at = datetime('now') WHERE id = ?").run(user.id); + + return c.json({ token, expiresIn: '30d' }); +}); + +// POST /auth/revoke โ€” invalidate all existing tokens by updating updated_at +authRoutes.post('/revoke', authMiddleware, (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + db.prepare("UPDATE users SET updated_at = datetime('now') WHERE id = ?").run(userId); + return c.json({ ok: true, message: 'All existing tokens invalidated' }); +}); diff --git a/mothership/src/api/routes/events.ts b/mothership/src/api/routes/events.ts new file mode 100644 index 0000000..021fbce --- /dev/null +++ b/mothership/src/api/routes/events.ts @@ -0,0 +1,71 @@ +import { Hono } from 'hono'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { verifyMembership } from '../middleware/workspace.js'; +import { bus } from '../../events/bus.js'; +import type { RegentEvent } from '../../db/schema.js'; + +export const eventRoutes = new Hono(); +eventRoutes.use('*', authMiddleware); + +// GET /workspaces/:wsId/events โ€” list events (viewer+) +eventRoutes.get('/', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const sessionId = c.req.query('sessionId'); + const limit = Math.min(parseInt(c.req.query('limit') || '100', 10) || 100, 1000); + + const sql = sessionId + ? 'SELECT * FROM events WHERE workspace_id = ? AND session_id = ? ORDER BY created_at DESC LIMIT ?' + : 'SELECT * FROM events WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?'; + const params = sessionId ? [ctx.wsId, sessionId, limit] : [ctx.wsId, limit]; + + return c.json(ctx.db.prepare(sql).all(...params) as RegentEvent[]); +}); + +// POST /workspaces/:wsId/events/bulk โ€” store events (member+), capped batch size +eventRoutes.post('/bulk', async (c) => { + const ctx = verifyMembership(c, 'member'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const { sessionId, events } = await c.req.json<{ + sessionId: string; + events: Array<{ title: string; summary: string; importance?: string; messageIndex?: number }>; + }>(); + + if (!sessionId || !events?.length) { + return c.json({ error: 'sessionId and events[] required' }, 400); + } + if (events.length > 200) { + return c.json({ error: 'Too many events (max 200 per batch)' }, 400); + } + + const insert = ctx.db.prepare(`INSERT INTO events (id, session_id, workspace_id, title, summary, importance, message_index) + VALUES (?, ?, ?, ?, ?, ?, ?)`); + + // Atomic: upsert session + store all events in one transaction + const stored: RegentEvent[] = []; + ctx.db.transaction(() => { + const existingSession = ctx.db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId); + if (!existingSession) { + ctx.db.prepare(`INSERT INTO sessions (id, workspace_id, status) VALUES (?, ?, 'active')`).run(sessionId, ctx.wsId); + } + + for (const evt of events) { + const id = newId(); + insert.run(id, sessionId, ctx.wsId, evt.title, evt.summary, evt.importance || 'medium', evt.messageIndex ?? null); + stored.push({ + id, session_id: sessionId, workspace_id: ctx.wsId, + title: evt.title, summary: evt.summary, + importance: (evt.importance || 'medium') as RegentEvent['importance'], + message_index: evt.messageIndex ?? null, + source_tab_id: null, created_at: new Date().toISOString(), + }); + } + })(); + + bus.emit('events:new', { workspaceId: ctx.wsId, sessionId, events: stored, sourceTabId: null }); + + return c.json({ stored: stored.length }, 201); +}); diff --git a/mothership/src/api/routes/invites.ts b/mothership/src/api/routes/invites.ts new file mode 100644 index 0000000..630aad3 --- /dev/null +++ b/mothership/src/api/routes/invites.ts @@ -0,0 +1,96 @@ +/** + * Workspace invite API โ€” generate and redeem pairing codes. + */ + +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { verifyMembership } from '../middleware/workspace.js'; +import { nanoid } from 'nanoid'; +import type { WorkspaceInvite } from '../../db/schema.js'; + +export const inviteRoutes = new Hono(); +inviteRoutes.use('*', authMiddleware); + +/** POST /workspaces/:wsId/invites โ€” generate invite code (admin+) */ +inviteRoutes.post('/', async (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found or insufficient permissions' }, 404); + + const { role, maxUses, expiresIn } = await c.req.json<{ + role?: 'admin' | 'member' | 'viewer'; + maxUses?: number; + expiresIn?: number; // hours + }>(); + + const validRoles = ['admin', 'member', 'viewer'] as const; + const inviteRole = validRoles.includes(role as any) ? role! : 'member'; + const uses = Math.max(1, Math.min(maxUses || 1, 100)); + + const code = nanoid(12); + const expiresAt = expiresIn + ? new Date(Date.now() + expiresIn * 60 * 60 * 1000).toISOString() + : null; + + ctx.db.prepare(`INSERT INTO workspace_invites (code, workspace_id, created_by, role, max_uses, expires_at) + VALUES (?, ?, ?, ?, ?, ?)`) + .run(code, ctx.wsId, ctx.userId, inviteRole, uses, expiresAt); + + return c.json({ code, role: inviteRole, maxUses: uses, expiresAt }, 201); +}); + +/** POST /auth/pair โ€” redeem an invite code (appended to auth routes) */ +export async function redeemInvite(c: any) { + const { userId } = c.get('auth'); + const { code } = await c.req.json() as { code: string }; + if (!code) return c.json({ error: 'code required' }, 400); + + const db = getDb(); + + const result = db.transaction(() => { + const invite = db.prepare('SELECT * FROM workspace_invites WHERE code = ?').get(code) as WorkspaceInvite | null; + if (!invite) return { error: 'Invalid invite code', status: 404 }; + if (invite.uses >= invite.max_uses) return { error: 'Invite code exhausted', status: 410 }; + if (invite.expires_at && new Date(invite.expires_at) < new Date()) return { error: 'Invite expired', status: 410 }; + + const existing = db.prepare('SELECT 1 FROM workspace_members WHERE workspace_id = ? AND user_id = ?') + .get(invite.workspace_id, userId); + if (existing) return { error: 'Already a member', status: 409 }; + + db.prepare('INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)') + .run(invite.workspace_id, userId, invite.role); + db.prepare('UPDATE workspace_invites SET uses = uses + 1 WHERE code = ?').run(code); + + db.prepare('INSERT INTO audit_log (id, user_id, action, resource_type, resource_id) VALUES (?, ?, ?, ?, ?)') + .run(newId(), userId, 'workspace_joined', 'workspace', invite.workspace_id); + + return { workspaceId: invite.workspace_id, role: invite.role }; + })(); + + if ('error' in result) return c.json({ error: result.error }, result.status as any); + return c.json(result); +} + +/** GET /workspaces/:wsId/invites โ€” list active invites (admin+) */ +inviteRoutes.get('/', (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const invites = ctx.db.prepare( + 'SELECT code, role, max_uses, uses, expires_at, created_at FROM workspace_invites WHERE workspace_id = ? ORDER BY created_at DESC' + ).all(ctx.wsId); + + return c.json(invites); +}); + +/** DELETE /workspaces/:wsId/invites/:code โ€” revoke invite (admin+) */ +inviteRoutes.delete('/:code', (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const result = ctx.db.prepare('DELETE FROM workspace_invites WHERE code = ? AND workspace_id = ?') + .run(c.req.param('code'), ctx.wsId); + if (result.changes === 0) return c.json({ error: 'Not found' }, 404); + return c.json({ ok: true }); +}); diff --git a/mothership/src/api/routes/mcp.ts b/mothership/src/api/routes/mcp.ts new file mode 100644 index 0000000..cfdaf2b --- /dev/null +++ b/mothership/src/api/routes/mcp.ts @@ -0,0 +1,101 @@ +/** + * MCP REST API โ€” manage MCP server configurations and connections. + * All mutating MCP operations require admin+ role. + */ + +import { Hono } from 'hono'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { verifyMembership } from '../middleware/workspace.js'; +import { connectServer, disconnectServer, getWorkspaceTools } from '../../mcp/pool.js'; +import type { McpServer } from '../../db/schema.js'; + +export const mcpRoutes = new Hono(); +mcpRoutes.use('*', authMiddleware); + +/** Verify server belongs to workspace โ€” returns server row or null */ +function getServerInWorkspace(db: any, serverId: string, wsId: string) { + return db.prepare('SELECT 1 FROM mcp_servers WHERE id = ? AND workspace_id = ?').get(serverId, wsId); +} + +/** GET /workspaces/:wsId/mcp โ€” list MCP server configs (viewer+) */ +mcpRoutes.get('/', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const servers = ctx.db.prepare('SELECT * FROM mcp_servers WHERE workspace_id = ? ORDER BY created_at DESC') + .all(ctx.wsId) as McpServer[]; + + return c.json(servers.map(s => ({ + id: s.id, name: s.name, transport: s.transport, + status: s.status, created_at: s.created_at, + tools: s.tools_cache ? JSON.parse(s.tools_cache).length : 0, + }))); +}); + +/** POST /workspaces/:wsId/mcp โ€” add MCP server config (admin+) */ +mcpRoutes.post('/', async (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const { name, transport, config } = await c.req.json<{ + name: string; transport: 'stdio' | 'sse' | 'streamable-http'; config: Record; + }>(); + if (!name || !transport || !config) return c.json({ error: 'name, transport, and config required' }, 400); + if (name.length > 256) return c.json({ error: 'name too long (max 256 chars)' }, 400); + + const id = newId(); + ctx.db.prepare(`INSERT INTO mcp_servers (id, workspace_id, name, transport, config) VALUES (?, ?, ?, ?, ?)`) + .run(id, ctx.wsId, name, transport, JSON.stringify(config)); + + return c.json({ id, name, transport, status: 'disconnected' }, 201); +}); + +/** POST /workspaces/:wsId/mcp/:id/connect (admin+) */ +mcpRoutes.post('/:id/connect', async (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const serverId = c.req.param('id'); + if (!getServerInWorkspace(ctx.db, serverId, ctx.wsId)) return c.json({ error: 'Server not found' }, 404); + + try { + const { tools } = await connectServer(serverId); + return c.json({ status: 'connected', tools }); + } catch (err: any) { + return c.json({ error: err.message }, 500); + } +}); + +/** POST /workspaces/:wsId/mcp/:id/disconnect (admin+) โ€” verify ownership */ +mcpRoutes.post('/:id/disconnect', (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const serverId = c.req.param('id'); + if (!getServerInWorkspace(ctx.db, serverId, ctx.wsId)) return c.json({ error: 'Server not found' }, 404); + + disconnectServer(serverId); + return c.json({ ok: true }); +}); + +/** DELETE /workspaces/:wsId/mcp/:id (admin+) */ +mcpRoutes.delete('/:id', (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const serverId = c.req.param('id'); + disconnectServer(serverId); + const result = ctx.db.prepare('DELETE FROM mcp_servers WHERE id = ? AND workspace_id = ?') + .run(serverId, ctx.wsId); + if (result.changes === 0) return c.json({ error: 'Not found' }, 404); + return c.json({ ok: true }); +}); + +/** GET /workspaces/:wsId/mcp/tools โ€” aggregated tool list (viewer+) */ +mcpRoutes.get('/tools', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + return c.json(getWorkspaceTools(ctx.wsId)); +}); diff --git a/mothership/src/api/routes/memory.ts b/mothership/src/api/routes/memory.ts new file mode 100644 index 0000000..c795d65 --- /dev/null +++ b/mothership/src/api/routes/memory.ts @@ -0,0 +1,89 @@ +/** + * Memory API โ€” search + CRUD for memory entries. + * Workspace-scoped, requires authentication. + */ + +import { Hono } from 'hono'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { verifyMembership } from '../middleware/workspace.js'; +import { hybridSearch } from '../../memory/search.js'; +import { embed, vectorToBlob } from '../../memory/embeddings.js'; + +export const memoryRoutes = new Hono(); +memoryRoutes.use('*', authMiddleware); + +/** POST /workspaces/:wsId/memory/search โ€” hybrid search (viewer+) */ +memoryRoutes.post('/search', async (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const { query, limit } = await c.req.json<{ query: string; limit?: number }>(); + if (!query) return c.json({ error: 'query required' }, 400); + + const results = await hybridSearch(ctx.userId, ctx.wsId, query, limit || 20); + + return c.json(results.map(r => ({ + id: r.entry.id, + content: r.entry.content, + source_type: r.entry.source_type, + session_id: r.entry.session_id, + event_id: r.entry.event_id, + created_at: r.entry.created_at, + score: r.score, + source: r.source, + }))); +}); + +/** GET /workspaces/:wsId/memory โ€” list recent entries (viewer+) */ +memoryRoutes.get('/', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const limit = Math.min(parseInt(c.req.query('limit') || '50', 10) || 50, 500); + const sessionId = c.req.query('sessionId'); + + const sql = sessionId + ? 'SELECT id, workspace_id, session_id, event_id, content, source_type, created_at FROM memory_entries WHERE workspace_id = ? AND session_id = ? ORDER BY created_at DESC LIMIT ?' + : 'SELECT id, workspace_id, session_id, event_id, content, source_type, created_at FROM memory_entries WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?'; + const params = sessionId ? [ctx.wsId, sessionId, limit] : [ctx.wsId, limit]; + + return c.json(ctx.db.prepare(sql).all(...params)); +}); + +/** POST /workspaces/:wsId/memory โ€” create entry (member+) */ +memoryRoutes.post('/', async (c) => { + const ctx = verifyMembership(c, 'member'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const { content, sourceType, sessionId, eventId } = await c.req.json<{ + content: string; sourceType?: string; sessionId?: string; eventId?: string; + }>(); + if (!content) return c.json({ error: 'content required' }, 400); + if (content.length > 65536) return c.json({ error: 'content too long (max 64KB)' }, 400); + + const id = newId(); + const type = sourceType === 'summary' ? 'summary' : sourceType === 'note' ? 'note' : 'event'; + + let embeddingBlob: Buffer | null = null; + const result = await embed(ctx.userId, content); + if (result) embeddingBlob = vectorToBlob(result.embedding); + + ctx.db.prepare(`INSERT INTO memory_entries (id, workspace_id, session_id, event_id, content, embedding, source_type) + VALUES (?, ?, ?, ?, ?, ?, ?)`) + .run(id, ctx.wsId, sessionId ?? null, eventId ?? null, content, embeddingBlob, type); + + return c.json({ id, source_type: type }, 201); +}); + +/** DELETE /workspaces/:wsId/memory/:id โ€” admin+ */ +memoryRoutes.delete('/:id', (c) => { + const ctx = verifyMembership(c, 'admin'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const result = ctx.db.prepare('DELETE FROM memory_entries WHERE id = ? AND workspace_id = ?') + .run(c.req.param('id'), ctx.wsId); + + if (result.changes === 0) return c.json({ error: 'Not found' }, 404); + return c.json({ ok: true }); +}); diff --git a/mothership/src/api/routes/notifications.ts b/mothership/src/api/routes/notifications.ts new file mode 100644 index 0000000..cdbb546 --- /dev/null +++ b/mothership/src/api/routes/notifications.ts @@ -0,0 +1,75 @@ +/** + * Notifications API โ€” list, mark read, and manage notifications. + */ + +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { bus } from '../../events/bus.js'; + +export const notificationRoutes = new Hono(); +notificationRoutes.use('*', authMiddleware); + +/** GET /notifications โ€” list with offset + limit pagination */ +notificationRoutes.get('/', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + const limit = Math.min(parseInt(c.req.query('limit') || '50', 10) || 50, 100); + const offset = Math.max(parseInt(c.req.query('offset') || '0', 10) || 0, 0); + const unreadOnly = c.req.query('unread') === 'true'; + + const sql = unreadOnly + ? 'SELECT * FROM notifications WHERE user_id = ? AND read = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?' + : 'SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?'; + + return c.json(db.prepare(sql).all(userId, limit, offset)); +}); + +/** POST /notifications/:id/read โ€” mark as read */ +notificationRoutes.post('/:id/read', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + db.prepare('UPDATE notifications SET read = 1 WHERE id = ? AND user_id = ?') + .run(c.req.param('id'), userId); + return c.json({ ok: true }); +}); + +/** POST /notifications/read-all โ€” mark all as read */ +notificationRoutes.post('/read-all', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + db.prepare('UPDATE notifications SET read = 1 WHERE user_id = ? AND read = 0').run(userId); + return c.json({ ok: true }); +}); + +/** GET /notifications/count โ€” unread count */ +notificationRoutes.get('/count', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + const row = db.prepare('SELECT COUNT(*) as count FROM notifications WHERE user_id = ? AND read = 0') + .get(userId) as { count: number }; + return c.json({ unread: row.count }); +}); + +/** Create a notification for a user (internal helper) */ +export function createNotification(userId: string, workspaceId: string, type: string, title: string, body?: string) { + const db = getDb(); + const id = newId(); + db.prepare(`INSERT INTO notifications (id, user_id, workspace_id, type, title, body) VALUES (?, ?, ?, ?, ?, ?)`) + .run(id, userId, workspaceId, type, title, body ?? null); + + bus.emit('notification:new', { userId, notification: { id, type, title, body, workspaceId } }); +} + +/** Notify all workspace members (except excludeUserId) */ +export function notifyWorkspaceMembers(workspaceId: string, type: string, title: string, body?: string, excludeUserId?: string) { + const db = getDb(); + const members = db.prepare('SELECT user_id FROM workspace_members WHERE workspace_id = ?') + .all(workspaceId) as { user_id: string }[]; + + for (const { user_id } of members) { + if (user_id === excludeUserId) continue; + createNotification(user_id, workspaceId, type, title, body); + } +} diff --git a/mothership/src/api/routes/sessions.ts b/mothership/src/api/routes/sessions.ts new file mode 100644 index 0000000..69c4fdb --- /dev/null +++ b/mothership/src/api/routes/sessions.ts @@ -0,0 +1,56 @@ +import { Hono } from 'hono'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { verifyMembership } from '../middleware/workspace.js'; +import type { Session } from '../../db/schema.js'; + +export const sessionRoutes = new Hono(); +sessionRoutes.use('*', authMiddleware); + +const VALID_STATUSES = new Set(['active', 'closed']); + +// GET /workspaces/:wsId/sessions โ€” viewer+ +sessionRoutes.get('/', (c) => { + const ctx = verifyMembership(c); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const status = c.req.query('status'); + if (status && !VALID_STATUSES.has(status)) return c.json({ error: 'Invalid status' }, 400); + + const sql = status + ? 'SELECT * FROM sessions WHERE workspace_id = ? AND status = ? ORDER BY updated_at DESC' + : 'SELECT * FROM sessions WHERE workspace_id = ? ORDER BY updated_at DESC'; + const params = status ? [ctx.wsId, status] : [ctx.wsId]; + + return c.json(ctx.db.prepare(sql).all(...params) as Session[]); +}); + +// POST /workspaces/:wsId/sessions โ€” member+ +sessionRoutes.post('/', async (c) => { + const ctx = verifyMembership(c, 'member'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const { externalId, name, url, hostname } = await c.req.json<{ + externalId?: string; name?: string; url?: string; hostname?: string; + }>(); + + const id = newId(); + ctx.db.prepare(`INSERT INTO sessions (id, workspace_id, external_id, name, url, hostname) + VALUES (?, ?, ?, ?, ?, ?)`).run(id, ctx.wsId, externalId ?? null, name ?? null, url ?? null, hostname ?? null); + + return c.json({ id, workspace_id: ctx.wsId, status: 'active' }, 201); +}); + +// PATCH /workspaces/:wsId/sessions/:id โ€” close session (member+), scoped to workspace +sessionRoutes.patch('/:id', async (c) => { + const ctx = verifyMembership(c, 'member'); + if (!ctx) return c.json({ error: 'Not found' }, 404); + + const { status } = await c.req.json<{ status: string }>(); + if (!status || !VALID_STATUSES.has(status)) return c.json({ error: 'Invalid status (must be active or closed)' }, 400); + + const result = ctx.db.prepare("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ? AND workspace_id = ?") + .run(status, c.req.param('id'), ctx.wsId); + if (result.changes === 0) return c.json({ error: 'Session not found' }, 404); + return c.json({ ok: true }); +}); diff --git a/mothership/src/api/routes/workspaces.ts b/mothership/src/api/routes/workspaces.ts new file mode 100644 index 0000000..4e243b2 --- /dev/null +++ b/mothership/src/api/routes/workspaces.ts @@ -0,0 +1,59 @@ +import { Hono } from 'hono'; +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { authMiddleware } from '../middleware/auth.js'; +import { checkRateLimit } from '../../utils/rateLimit.js'; +import type { Workspace } from '../../db/schema.js'; + +export const workspaceRoutes = new Hono(); +workspaceRoutes.use('*', authMiddleware); + +// GET /workspaces โ€” list user's workspaces +workspaceRoutes.get('/', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + const rows = db.prepare(` + SELECT w.* FROM workspaces w + JOIN workspace_members wm ON wm.workspace_id = w.id + WHERE wm.user_id = ? + ORDER BY w.created_at DESC + `).all(userId) as Workspace[]; + return c.json(rows); +}); + +// POST /workspaces โ€” rate limited +workspaceRoutes.post('/', async (c) => { + const { userId } = c.get('auth'); + const { name } = await c.req.json<{ name: string }>(); + if (!name || name.length > 128) return c.json({ error: 'Name required (max 128 chars)' }, 400); + + // Rate limit workspace creation + if (!checkRateLimit(userId, 'workspace_create', 5)) { + return c.json({ error: 'Too many workspaces created, try again later' }, 429); + } + + const db = getDb(); + const id = newId(); + + // Atomic: create workspace + owner membership in one transaction + db.transaction(() => { + db.prepare('INSERT INTO workspaces (id, name, owner_id) VALUES (?, ?, ?)').run(id, name, userId); + db.prepare('INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)').run(id, userId, 'owner'); + })(); + + return c.json({ id, name, owner_id: userId }, 201); +}); + +// GET /workspaces/:id +workspaceRoutes.get('/:id', (c) => { + const { userId } = c.get('auth'); + const db = getDb(); + const ws = db.prepare(` + SELECT w.* FROM workspaces w + JOIN workspace_members wm ON wm.workspace_id = w.id + WHERE w.id = ? AND wm.user_id = ? + `).get(c.req.param('id'), userId) as Workspace | undefined; + + if (!ws) return c.json({ error: 'Not found' }, 404); + return c.json(ws); +}); diff --git a/mothership/src/config.ts b/mothership/src/config.ts new file mode 100644 index 0000000..775ee02 --- /dev/null +++ b/mothership/src/config.ts @@ -0,0 +1,19 @@ +import { resolve } from 'node:path'; +import { randomBytes } from 'node:crypto'; + +let jwtSecret = process.env.JWT_SECRET; +if (!jwtSecret) { + if (process.env.NODE_ENV === 'production') { + throw new Error('JWT_SECRET environment variable is required in production'); + } + // Ephemeral random secret for dev โ€” tokens won't survive restarts + jwtSecret = randomBytes(32).toString('hex'); + console.warn('[WARN] No JWT_SECRET set โ€” using random ephemeral secret (tokens will not persist across restarts)'); +} + +export const config = { + port: parseInt(process.env.PORT || '3001', 10), + jwtSecret, + dbPath: resolve(process.env.DB_PATH || './data/mothership.db'), + logLevel: process.env.LOG_LEVEL || 'info', +} as const; diff --git a/mothership/src/db/index.ts b/mothership/src/db/index.ts new file mode 100644 index 0000000..311277f --- /dev/null +++ b/mothership/src/db/index.ts @@ -0,0 +1,57 @@ +import Database from 'better-sqlite3'; +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as sqliteVec from 'sqlite-vec'; +import { config } from '../config.js'; +import { log } from '../utils/logger.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +let _db: Database.Database | null = null; + +export function getDb(): Database.Database { + if (_db) return _db; + + _db = new Database(config.dbPath); + _db.pragma('journal_mode = WAL'); + _db.pragma('foreign_keys = ON'); + _db.pragma('busy_timeout = 5000'); + + // Load sqlite-vec extension for vector search + sqliteVec.load(_db); + + runMigrations(_db); + log.info('Database initialized at %s (sqlite-vec loaded)', config.dbPath); + return _db; +} + +function runMigrations(db: Database.Database) { + db.exec(`CREATE TABLE IF NOT EXISTS _migrations ( + name TEXT PRIMARY KEY, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + )`); + + const applied = new Set( + db.prepare('SELECT name FROM _migrations').all().map((r: any) => r.name) + ); + + const migrationsDir = resolve(__dirname, 'migrations'); + const files = ['001_foundation.sql', '002_memory_fts.sql', '003_agents_mcp.sql', '004_multiuser.sql']; + + for (const file of files) { + if (applied.has(file)) continue; + const sql = readFileSync(resolve(migrationsDir, file), 'utf-8'); + // Atomic: apply migration SQL + record in single transaction + db.transaction(() => { + db.exec(sql); + db.prepare('INSERT INTO _migrations (name) VALUES (?)').run(file); + })(); + log.info('Applied migration: %s', file); + } +} + +export function closeDb() { + _db?.close(); + _db = null; +} diff --git a/mothership/src/db/migrations/001_foundation.sql b/mothership/src/db/migrations/001_foundation.sql new file mode 100644 index 0000000..42b4d6c --- /dev/null +++ b/mothership/src/db/migrations/001_foundation.sql @@ -0,0 +1,75 @@ +-- Phase 1: Foundation schema + +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS workspaces ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + owner_id TEXT NOT NULL REFERENCES users(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS workspace_members ( + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('owner','admin','member','viewer')), + joined_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (workspace_id, user_id) +); + +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + external_id TEXT, + name TEXT, + url TEXT, + hostname TEXT, + status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','closed')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + title TEXT NOT NULL, + summary TEXT NOT NULL, + importance TEXT NOT NULL DEFAULT 'medium' CHECK(importance IN ('high','medium','low')), + message_index INTEGER, + source_tab_id TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + content TEXT NOT NULL, + role TEXT DEFAULT 'assistant', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS audit_log ( + id TEXT PRIMARY KEY, + user_id TEXT REFERENCES users(id), + action TEXT NOT NULL, + resource_type TEXT, + resource_id TEXT, + detail TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON sessions(workspace_id); +CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id); +CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_id); +CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at); +CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id); +CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id); diff --git a/mothership/src/db/migrations/002_memory_fts.sql b/mothership/src/db/migrations/002_memory_fts.sql new file mode 100644 index 0000000..95f45d1 --- /dev/null +++ b/mothership/src/db/migrations/002_memory_fts.sql @@ -0,0 +1,51 @@ +-- Phase 2: Memory & Context โ€” FTS5 + vector embeddings + +CREATE TABLE IF NOT EXISTS memory_entries ( + id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL, + event_id TEXT REFERENCES events(id) ON DELETE SET NULL, + content TEXT NOT NULL, + embedding BLOB, + source_type TEXT NOT NULL DEFAULT 'event' CHECK(source_type IN ('event','note','summary')), + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- FTS5 virtual table for full-text search +CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5( + content, + content_rowid='rowid', + tokenize='porter unicode61' +); + +-- Trigger: auto-populate FTS on insert +CREATE TRIGGER IF NOT EXISTS memory_fts_insert AFTER INSERT ON memory_entries BEGIN + INSERT INTO memory_fts(rowid, content) VALUES (NEW.rowid, NEW.content); +END; + +-- Trigger: auto-update FTS on update (FTS5 requires delete+insert, not UPDATE) +CREATE TRIGGER IF NOT EXISTS memory_fts_update AFTER UPDATE OF content ON memory_entries BEGIN + DELETE FROM memory_fts WHERE rowid = OLD.rowid; + INSERT INTO memory_fts(rowid, content) VALUES (NEW.rowid, NEW.content); +END; + +-- Trigger: auto-delete FTS on delete +CREATE TRIGGER IF NOT EXISTS memory_fts_delete AFTER DELETE ON memory_entries BEGIN + DELETE FROM memory_fts WHERE rowid = OLD.rowid; +END; + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_memory_workspace ON memory_entries(workspace_id); +CREATE INDEX IF NOT EXISTS idx_memory_session ON memory_entries(session_id); +CREATE INDEX IF NOT EXISTS idx_memory_event ON memory_entries(event_id); +CREATE INDEX IF NOT EXISTS idx_memory_created ON memory_entries(created_at); + +-- Provider credentials table (encrypted, per-user session-scoped) +CREATE TABLE IF NOT EXISTS provider_credentials ( + user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + provider TEXT NOT NULL, + api_key TEXT NOT NULL, + api_url TEXT, + model TEXT, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); diff --git a/mothership/src/db/migrations/003_agents_mcp.sql b/mothership/src/db/migrations/003_agents_mcp.sql new file mode 100644 index 0000000..60056a4 --- /dev/null +++ b/mothership/src/db/migrations/003_agents_mcp.sql @@ -0,0 +1,50 @@ +-- Phase 3: Agent Runtime + MCP + +CREATE TABLE IF NOT EXISTS agents ( + id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + name TEXT NOT NULL, + system_prompt TEXT, + mcp_servers TEXT, -- JSON array of MCP server IDs + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS agent_runs ( + id TEXT PRIMARY KEY, + agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id), + input TEXT NOT NULL, + output TEXT, + session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL, + status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running','completed','failed','cancelled')), + started_at TEXT NOT NULL DEFAULT (datetime('now')), + finished_at TEXT, + duration_ms INTEGER +); + +CREATE TABLE IF NOT EXISTS mcp_servers ( + id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + name TEXT NOT NULL, + transport TEXT NOT NULL CHECK(transport IN ('stdio','sse','streamable-http')), + config TEXT NOT NULL, -- JSON: { command, args, env } for stdio, { url } for sse/http + status TEXT NOT NULL DEFAULT 'disconnected' CHECK(status IN ('connected','disconnected','error')), + tools_cache TEXT, -- JSON array of cached tool schemas + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_agents_workspace ON agents(workspace_id); +CREATE INDEX IF NOT EXISTS idx_runs_agent ON agent_runs(agent_id); +CREATE INDEX IF NOT EXISTS idx_runs_workspace ON agent_runs(workspace_id); +CREATE INDEX IF NOT EXISTS idx_runs_status ON agent_runs(status); +CREATE INDEX IF NOT EXISTS idx_runs_user ON agent_runs(user_id); +CREATE INDEX IF NOT EXISTS idx_mcp_workspace ON mcp_servers(workspace_id); + +-- Auto-update timestamp trigger +CREATE TRIGGER IF NOT EXISTS agents_updated_at AFTER UPDATE ON agents BEGIN + UPDATE agents SET updated_at = datetime('now') WHERE id = NEW.id; +END; diff --git a/mothership/src/db/migrations/004_multiuser.sql b/mothership/src/db/migrations/004_multiuser.sql new file mode 100644 index 0000000..b320ca5 --- /dev/null +++ b/mothership/src/db/migrations/004_multiuser.sql @@ -0,0 +1,41 @@ +-- Phase 4: Multi-user collaboration + production hardening + +-- Workspace invite/pairing codes +CREATE TABLE IF NOT EXISTS workspace_invites ( + code TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + created_by TEXT NOT NULL REFERENCES users(id), + role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('admin','member','viewer')), + max_uses INTEGER NOT NULL DEFAULT 1, + uses INTEGER NOT NULL DEFAULT 0, + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Rate limiting token bucket +CREATE TABLE IF NOT EXISTS rate_limits ( + user_id TEXT NOT NULL, + bucket TEXT NOT NULL, -- e.g. 'agent_runs', 'search', 'api' + tokens INTEGER NOT NULL DEFAULT 100, + max_tokens INTEGER NOT NULL DEFAULT 100, + refill_rate INTEGER NOT NULL DEFAULT 10, -- tokens per minute + last_refill TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (user_id, bucket) +); + +-- Notifications +CREATE TABLE IF NOT EXISTS notifications ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + type TEXT NOT NULL, -- 'event', 'agent_complete', 'member_joined' + title TEXT NOT NULL, + body TEXT, + read INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_invites_workspace ON workspace_invites(workspace_id); +CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id); +CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(user_id, read); diff --git a/mothership/src/db/schema.ts b/mothership/src/db/schema.ts new file mode 100644 index 0000000..1ed9721 --- /dev/null +++ b/mothership/src/db/schema.ts @@ -0,0 +1,153 @@ +// TypeScript types matching the SQLite schema + +export interface User { + id: string; + username: string; + password_hash: string; + created_at: string; + updated_at: string; +} + +export interface Workspace { + id: string; + name: string; + owner_id: string; + created_at: string; + updated_at: string; +} + +export interface WorkspaceMember { + workspace_id: string; + user_id: string; + role: 'owner' | 'admin' | 'member' | 'viewer'; + joined_at: string; +} + +export interface Session { + id: string; + workspace_id: string; + external_id: string | null; + name: string | null; + url: string | null; + hostname: string | null; + status: 'active' | 'closed'; + created_at: string; + updated_at: string; +} + +export interface RegentEvent { + id: string; + session_id: string; + workspace_id: string; + title: string; + summary: string; + importance: 'high' | 'medium' | 'low'; + message_index: number | null; + source_tab_id: string | null; + created_at: string; +} + +export interface Message { + id: string; + session_id: string; + content: string; + role: string | null; + created_at: string; +} + +export interface AuditEntry { + id: string; + user_id: string | null; + action: string; + resource_type: string | null; + resource_id: string | null; + detail: string | null; + created_at: string; +} + +export interface MemoryEntry { + id: string; + workspace_id: string; + session_id: string | null; + event_id: string | null; + content: string; + embedding: Buffer | null; + source_type: 'event' | 'note' | 'summary'; + created_at: string; +} + +export interface ProviderCredential { + user_id: string; + provider: string; + api_key: string; + api_url: string | null; + model: string | null; + updated_at: string; +} + +export interface Agent { + id: string; + workspace_id: string; + name: string; + system_prompt: string | null; + mcp_servers: string | null; // JSON array of MCP server IDs + created_at: string; + updated_at: string; +} + +export interface AgentRun { + id: string; + agent_id: string; + workspace_id: string; + user_id: string; + input: string; + output: string | null; + session_id: string | null; + status: 'running' | 'completed' | 'failed' | 'cancelled'; + started_at: string; + finished_at: string | null; + duration_ms: number | null; +} + +export interface McpServer { + id: string; + workspace_id: string; + name: string; + transport: 'stdio' | 'sse' | 'streamable-http'; + config: string; // JSON + status: 'connected' | 'disconnected' | 'error'; + tools_cache: string | null; // JSON + created_at: string; + updated_at: string; +} + +export interface WorkspaceInvite { + code: string; + workspace_id: string; + created_by: string; + role: 'admin' | 'member' | 'viewer'; + max_uses: number; + uses: number; + expires_at: string | null; + created_at: string; +} + +export interface RateLimit { + user_id: string; + bucket: string; + tokens: number; + max_tokens: number; + refill_rate: number; + last_refill: string; +} + +export interface Notification { + id: string; + user_id: string; + workspace_id: string; + type: string; + title: string; + body: string | null; + read: number; + created_at: string; +} diff --git a/mothership/src/events/bus.ts b/mothership/src/events/bus.ts new file mode 100644 index 0000000..fddb365 --- /dev/null +++ b/mothership/src/events/bus.ts @@ -0,0 +1,19 @@ +import { EventEmitter } from 'node:events'; + +/** + * Singleton event bus for broadcasting DB changes to WebSocket subscribers. + * Pattern borrowed from OpenClaw's architecture. + * + * Events: + * 'events:new' โ†’ { workspaceId, sessionId, events[] } + * 'session:open' โ†’ { workspaceId, session } + * 'session:close' โ†’ { workspaceId, sessionId } + */ +class Bus extends EventEmitter { + constructor() { + super(); + this.setMaxListeners(1000); // Support many concurrent WS connections + } +} + +export const bus = new Bus(); diff --git a/mothership/src/index.ts b/mothership/src/index.ts new file mode 100644 index 0000000..77e26c3 --- /dev/null +++ b/mothership/src/index.ts @@ -0,0 +1,34 @@ +import { serve } from '@hono/node-server'; +import { config } from './config.js'; +import { api } from './api/index.js'; +import { attachWebSocket } from './ws/gateway.js'; +import { getDb, closeDb } from './db/index.js'; +import { startRetention, stopRetention } from './memory/retention.js'; +import { disconnectAll } from './mcp/pool.js'; +import { log } from './utils/logger.js'; + +// Initialize database (runs migrations on first start) +getDb(); + +// Start data retention scheduler +startRetention(); + +// Start HTTP server +const server = serve({ fetch: api.fetch, port: config.port }, (info) => { + log.info(`Mothership listening on http://localhost:${info.port}`); +}); + +// Attach WebSocket to the same HTTP server +attachWebSocket(server as any); + +// Graceful shutdown +for (const sig of ['SIGINT', 'SIGTERM'] as const) { + process.on(sig, async () => { + log.info('Shutting down...'); + stopRetention(); + await disconnectAll(); + closeDb(); + await new Promise((resolve) => server.close(() => resolve())); + process.exit(0); + }); +} diff --git a/mothership/src/mcp/client.ts b/mothership/src/mcp/client.ts new file mode 100644 index 0000000..c45200c --- /dev/null +++ b/mothership/src/mcp/client.ts @@ -0,0 +1,283 @@ +/** + * MCP Client โ€” lightweight wrapper for MCP server communication. + * Supports stdio (subprocess) and HTTP/SSE transports. + * No dependency on @modelcontextprotocol/sdk โ€” uses raw JSON-RPC 2.0. + */ + +import { spawn, type ChildProcess } from 'node:child_process'; +import { log } from '../utils/logger.js'; + +/** Allowed MCP commands โ€” only bare names, no paths allowed */ +const ALLOWED_COMMANDS = new Set([ + 'npx', 'node', 'python', 'python3', 'uvx', 'docker', + 'mcp-server-fetch', 'mcp-server-filesystem', 'mcp-server-github', + 'mcp-server-postgres', 'mcp-server-sqlite', 'mcp-server-memory', +]); + +/** Blocked interpreter flags that enable arbitrary code execution */ +const BLOCKED_FLAGS = new Set(['-e', '--eval', '-c', '--command', '--exec', '-i', '--interactive']); + +/** Env vars that cannot be overridden (privilege escalation prevention) */ +const BLOCKED_ENV_KEYS = new Set([ + 'PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'NODE_OPTIONS', + 'DYLD_INSERT_LIBRARIES', 'PYTHONPATH', 'HOME', 'USER', +]); + +/** Block requests to private/internal IP ranges (SSRF protection) */ +function validateUrl(url: string) { + const parsed = new URL(url); + const hostname = parsed.hostname; + + // Block credentials in URL + if (parsed.username || parsed.password) { + throw new Error('Blocked: credentials in URL not allowed'); + } + + // Comprehensive private IP check including IPv6 + if (/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.|localhost$|::1$|\[::1\]$|fe80:|fc00:|fd00:|0x|0[0-7])/i.test(hostname)) { + throw new Error(`Blocked: private/internal address ${hostname}`); + } + // Block cloud metadata hostnames + if (/^(metadata\.google\.internal|instance-data|169\.254\.169\.254)$/i.test(hostname)) { + throw new Error(`Blocked: cloud metadata address ${hostname}`); + } + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + throw new Error(`Blocked: unsupported protocol ${parsed.protocol}`); + } +} + +const REQUEST_TIMEOUT = 30_000; + +export interface McpTool { + name: string; + description: string; + inputSchema: Record; +} + +export interface McpToolResult { + content: Array<{ type: string; text?: string }>; + isError?: boolean; +} + +interface JsonRpcRequest { + jsonrpc: '2.0'; + id: number; + method: string; + params?: Record; +} + +interface JsonRpcResponse { + jsonrpc: '2.0'; + id: number; + result?: any; + error?: { code: number; message: string }; +} + +export interface McpClientConfig { + transport: 'stdio' | 'sse' | 'streamable-http'; + command?: string; + args?: string[]; + env?: Record; + url?: string; +} + +export class McpClient { + private config: McpClientConfig; + private process: ChildProcess | null = null; + private nextId = 1; + private pending = new Map void; reject: (e: Error) => void }>(); + private buffer = ''; + private _tools: McpTool[] = []; + private _connected = false; + + constructor(config: McpClientConfig) { + this.config = config; + } + + get connected() { return this._connected; } + get tools() { return this._tools; } + + /** Connect to MCP server */ + async connect(): Promise { + if (this.config.transport === 'stdio') return this.connectStdio(); + return this.connectHttp(); + } + + /** Disconnect from MCP server */ + disconnect() { + if (this.process) { + const proc = this.process; + proc.kill('SIGTERM'); + // Escalate to SIGKILL after 5s if process doesn't exit + const forceKill = setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, 5000); + proc.once('exit', () => clearTimeout(forceKill)); + this.process = null; + } + this._connected = false; + this.pending.forEach(p => p.reject(new Error('Disconnected'))); + this.pending.clear(); + } + + /** Call an MCP tool */ + async callTool(name: string, args: Record): Promise { + return await this.request('tools/call', { name, arguments: args }) as McpToolResult; + } + + /** List tools from the server */ + async listTools(): Promise { + const result = await this.request('tools/list', {}); + this._tools = (result?.tools || []) as McpTool[]; + return this._tools; + } + + // โ”€โ”€ stdio transport โ”€โ”€ + + private async connectStdio(): Promise { + const { command, args = [], env } = this.config; + if (!command) throw new Error('stdio transport requires command'); + + // Security: reject absolute/relative paths โ€” only bare command names via PATH resolution + if (command.includes('/') || command.includes('\\')) { + throw new Error('Blocked: absolute/relative paths not allowed โ€” use bare command names only'); + } + if (!ALLOWED_COMMANDS.has(command)) { + throw new Error(`Blocked: command '${command}' not in allowlist. Allowed: ${[...ALLOWED_COMMANDS].join(', ')}`); + } + + // Security: block dangerous interpreter flags that allow arbitrary code execution + for (const arg of args) { + if (BLOCKED_FLAGS.has(arg)) { + throw new Error(`Blocked: dangerous flag '${arg}' not allowed in MCP args`); + } + } + + // Security: strip blocked env vars + const safeEnv: Record = {}; + if (env) { + for (const [k, v] of Object.entries(env)) { + if (!BLOCKED_ENV_KEYS.has(k.toUpperCase())) safeEnv[k] = v; + else log.warn({ key: k }, 'MCP: blocked env var override'); + } + } + + this.process = spawn(command, args, { + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, ...safeEnv }, + }); + + this.process.stdout!.on('data', (chunk: Buffer) => { + this.buffer += chunk.toString(); + this.processBuffer(); + }); + + this.process.stderr!.on('data', (chunk: Buffer) => { + log.debug({ stderr: chunk.toString().trim() }, 'MCP stderr'); + }); + + this.process.on('exit', (code) => { + log.info({ code }, 'MCP process exited'); + this._connected = false; + }); + + // Initialize + await this.request('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'regent-mothership', version: '0.1.0' }, + }); + + this.notify('notifications/initialized', {}); + this._connected = true; + return this.listTools(); + } + + // โ”€โ”€ HTTP/SSE transport โ”€โ”€ + + private async connectHttp(): Promise { + const { url } = this.config; + if (!url) throw new Error('HTTP transport requires url'); + + // Security: block private/internal URLs + validateUrl(url); + + this._connected = true; + + await this.httpRequest('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'regent-mothership', version: '0.1.0' }, + }); + + return this.listTools(); + } + + // โ”€โ”€ JSON-RPC helpers โ”€โ”€ + + private request(method: string, params: Record): Promise { + if (this.config.transport !== 'stdio') return this.httpRequest(method, params); + + return new Promise((resolve, reject) => { + if (!this.process?.stdin?.writable) return reject(new Error('Not connected')); + + const id = this.nextId++; + const req: JsonRpcRequest = { jsonrpc: '2.0', id, method, params }; + + this.pending.set(id, { resolve, reject }); + this.process.stdin.write(JSON.stringify(req) + '\n'); + + setTimeout(() => { + if (this.pending.has(id)) { + this.pending.delete(id); + reject(new Error(`MCP timeout: ${method}`)); + } + }, REQUEST_TIMEOUT); + }); + } + + private async httpRequest(method: string, params: Record): Promise { + // Re-validate URL on every request (defense-in-depth against config mutation) + validateUrl(this.config.url!); + + const id = this.nextId++; + const req: JsonRpcRequest = { jsonrpc: '2.0', id, method, params }; + + const res = await fetch(this.config.url!, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + signal: AbortSignal.timeout(REQUEST_TIMEOUT), + }); + + if (!res.ok) throw new Error(`MCP HTTP error: ${res.status}`); + + const response = await res.json() as JsonRpcResponse; + if (response.error) throw new Error(`MCP error: ${response.error.message}`); + return response.result; + } + + private notify(method: string, params: Record) { + if (this.process?.stdin?.writable) { + this.process.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n'); + } + } + + private processBuffer() { + const lines = this.buffer.split('\n'); + this.buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line) as JsonRpcResponse; + if (msg.id != null && this.pending.has(msg.id)) { + const handler = this.pending.get(msg.id)!; + this.pending.delete(msg.id); + if (msg.error) handler.reject(new Error(msg.error.message)); + else handler.resolve(msg.result); + } + } catch { + log.debug({ line }, 'Non-JSON MCP output'); + } + } + } +} diff --git a/mothership/src/mcp/pool.ts b/mothership/src/mcp/pool.ts new file mode 100644 index 0000000..2f0a433 --- /dev/null +++ b/mothership/src/mcp/pool.ts @@ -0,0 +1,116 @@ +/** + * MCP Connection Pool โ€” per-workspace MCP client management. + * Lazily connects, caches tool schemas, health checks. + */ + +import { getDb } from '../db/index.js'; +import { McpClient, type McpTool, type McpClientConfig } from './client.js'; +import { log } from '../utils/logger.js'; +import type { McpServer } from '../db/schema.js'; + +/** Active MCP client instances keyed by server ID */ +const clients = new Map(); + +/** Per-server connection lock to prevent concurrent double-spawn (M13) */ +const connecting = new Map>(); + +/** Connect to an MCP server (with dedup lock) */ +export async function connectServer(serverId: string): Promise<{ tools: McpTool[] }> { + // If already connecting, return the in-flight promise + const inflight = connecting.get(serverId); + if (inflight) return inflight; + + const promise = doConnect(serverId); + connecting.set(serverId, promise); + try { + return await promise; + } finally { + connecting.delete(serverId); + } +} + +async function doConnect(serverId: string): Promise<{ tools: McpTool[] }> { + const db = getDb(); + const server = db.prepare('SELECT * FROM mcp_servers WHERE id = ?').get(serverId) as McpServer | null; + if (!server) throw new Error(`MCP server not found: ${serverId}`); + + disconnectServer(serverId); + + const config = JSON.parse(server.config) as McpClientConfig; + config.transport = server.transport; + + const client = new McpClient(config); + + try { + const tools = await client.connect(); + clients.set(serverId, client); + + db.prepare("UPDATE mcp_servers SET status = 'connected', tools_cache = ?, updated_at = datetime('now') WHERE id = ?") + .run(JSON.stringify(tools), serverId); + + log.info({ serverId, toolCount: tools.length }, 'MCP server connected'); + return { tools }; + } catch (err) { + db.prepare("UPDATE mcp_servers SET status = 'error', updated_at = datetime('now') WHERE id = ?") + .run(serverId); + throw err; + } +} + +/** Disconnect an MCP client */ +export function disconnectServer(serverId: string) { + const client = clients.get(serverId); + if (client) { + client.disconnect(); + clients.delete(serverId); + } + + const db = getDb(); + db.prepare("UPDATE mcp_servers SET status = 'disconnected', updated_at = datetime('now') WHERE id = ?") + .run(serverId); +} + +/** Get an active MCP client */ +export function getClient(serverId: string): McpClient | undefined { + return clients.get(serverId); +} + +/** Get all tools from all connected MCP servers in a workspace */ +export function getWorkspaceTools(workspaceId: string): Array { + const db = getDb(); + const servers = db.prepare("SELECT * FROM mcp_servers WHERE workspace_id = ? AND status = 'connected'") + .all(workspaceId) as McpServer[]; + + const tools: Array = []; + for (const server of servers) { + const client = clients.get(server.id); + // Only return tools from live connected clients (M12 fix: skip stale cache for known-disconnected clients) + if (client?.connected) { + for (const tool of client.tools) { + tools.push({ ...tool, serverId: server.id }); + } + } else if (!client && server.tools_cache) { + // No client instance โ€” server was previously connected, use cache + try { + const cached = JSON.parse(server.tools_cache) as McpTool[]; + for (const tool of cached) tools.push({ ...tool, serverId: server.id }); + } catch (err) { + log.debug({ err, serverId: server.id }, 'Corrupted tools_cache'); + } + } + } + + return tools; +} + +/** Call a tool on the appropriate MCP server */ +export async function callTool(serverId: string, name: string, args: Record) { + const client = clients.get(serverId); + if (!client?.connected) throw new Error(`MCP server ${serverId} not connected`); + return client.callTool(name, args); +} + +/** Disconnect all clients (for shutdown) */ +export function disconnectAll() { + for (const [id] of clients) disconnectServer(id); +} diff --git a/mothership/src/memory/embeddings.ts b/mothership/src/memory/embeddings.ts new file mode 100644 index 0000000..2eb7822 --- /dev/null +++ b/mothership/src/memory/embeddings.ts @@ -0,0 +1,103 @@ +/** + * Provider-agnostic embedding client. + * Uses the user's own API provider (OpenAI-compatible /v1/embeddings format). + * Credentials stored per-user, API keys encrypted at rest (AES-256-GCM). + */ + +import { getDb } from '../db/index.js'; +import { log } from '../utils/logger.js'; +import { encryptSecret, decryptSecret } from '../utils/crypto.js'; +import type { ProviderCredential } from '../db/schema.js'; + +/** Default embedding model per provider */ +const PROVIDER_DEFAULTS: Record = { + deepseek: { url: 'https://api.deepseek.com/v1', model: 'deepseek-embedding' }, + openrouter: { url: 'https://openrouter.ai/api/v1', model: 'openai/text-embedding-3-small' }, + siliconflow: { url: 'https://api.siliconflow.cn/v1', model: 'BAAI/bge-m3' }, + openai: { url: 'https://api.openai.com/v1', model: 'text-embedding-3-small' }, +}; + +export interface EmbeddingResult { + embedding: number[]; + dimensions: number; +} + +/** Get user's provider credentials from DB โ€” decrypts API key */ +export function getProviderCredentials(userId: string): ProviderCredential | null { + const db = getDb(); + const row = db.prepare('SELECT * FROM provider_credentials WHERE user_id = ?').get(userId) as ProviderCredential | null; + if (!row) return null; + + // Decrypt the API key (handle legacy plaintext keys gracefully) + try { + row.api_key = row.api_key.includes(':') ? decryptSecret(row.api_key) : row.api_key; + } catch { + // Legacy plaintext key โ€” will be re-encrypted on next upsert + } + return row; +} + +/** Store/update provider credentials โ€” encrypts API key at rest */ +export function upsertProviderCredentials(userId: string, creds: { provider: string; apiKey: string; apiUrl?: string; model?: string }) { + const db = getDb(); + const encryptedKey = encryptSecret(creds.apiKey); + db.prepare(`INSERT INTO provider_credentials (user_id, provider, api_key, api_url, model, updated_at) + VALUES (?, ?, ?, ?, ?, datetime('now')) + ON CONFLICT(user_id) DO UPDATE SET provider=excluded.provider, api_key=excluded.api_key, + api_url=excluded.api_url, model=excluded.model, updated_at=datetime('now')`) + .run(userId, creds.provider, encryptedKey, creds.apiUrl ?? null, creds.model ?? null); +} + +/** Generate embedding vector for text using user's configured provider */ +export async function embed(userId: string, text: string): Promise { + const creds = getProviderCredentials(userId); + if (!creds) return null; + + const defaults = PROVIDER_DEFAULTS[creds.provider] ?? {}; + const baseUrl = (creds.api_url || defaults.url || '').replace(/\/+$/, ''); + const model = creds.model || defaults.model; + + if (!baseUrl || !model) { + log.warn({ userId, provider: creds.provider }, 'No embedding URL/model configured'); + return null; + } + + try { + const res = await fetch(`${baseUrl}/embeddings`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${creds.api_key}`, + }, + body: JSON.stringify({ input: text, model }), + }); + + if (!res.ok) { + log.warn({ status: res.status, provider: creds.provider }, 'Embedding API error'); + return null; + } + + const data = await res.json() as { data: Array<{ embedding: number[] }> }; + const vec = data.data?.[0]?.embedding; + if (!vec?.length) return null; + + return { embedding: vec, dimensions: vec.length }; + } catch (err) { + log.warn({ err, provider: creds.provider }, 'Embedding request failed'); + return null; + } +} + +/** Serialize float32 array to Buffer for sqlite-vec storage */ +export function vectorToBlob(vec: number[]): Buffer { + const buf = Buffer.alloc(vec.length * 4); + for (let i = 0; i < vec.length; i++) buf.writeFloatLE(vec[i], i * 4); + return buf; +} + +/** Deserialize Buffer back to float32 array */ +export function blobToVector(buf: Buffer): number[] { + const vec: number[] = []; + for (let i = 0; i < buf.length; i += 4) vec.push(buf.readFloatLE(i)); + return vec; +} diff --git a/mothership/src/memory/retention.ts b/mothership/src/memory/retention.ts new file mode 100644 index 0000000..3fe8edf --- /dev/null +++ b/mothership/src/memory/retention.ts @@ -0,0 +1,47 @@ +/** + * Data retention โ€” periodic cleanup of old records. + * Runs as a simple interval timer (no cron dependency needed). + */ + +import { getDb } from '../db/index.js'; +import { log } from '../utils/logger.js'; + +const RETENTION_INTERVAL = 6 * 60 * 60 * 1000; // Run every 6 hours + +const POLICIES = [ + { table: 'events', column: 'created_at', days: 90 }, + { table: 'messages', column: 'created_at', days: 30 }, + { table: 'memory_entries', column: 'created_at', days: 180 }, + { table: 'audit_log', column: 'created_at', days: 365 }, +] as const; + +/** Run one retention pass โ€” delete rows older than policy threshold */ +export function runRetention() { + const db = getDb(); + + for (const { table, column, days } of POLICIES) { + const result = db.prepare( + `DELETE FROM ${table} WHERE ${column} < datetime('now', '-${days} days')` + ).run(); + + if (result.changes > 0) { + log.info({ table, deleted: result.changes, days }, 'Retention cleanup'); + } + } +} + +let timer: ReturnType | null = null; + +/** Start periodic retention (idempotent) */ +export function startRetention() { + if (timer) return; + runRetention(); // Run immediately on start + timer = setInterval(runRetention, RETENTION_INTERVAL); + log.info('Retention scheduler started (every 6h)'); +} + +/** Stop retention timer */ +export function stopRetention() { + if (timer) clearInterval(timer); + timer = null; +} diff --git a/mothership/src/memory/search.ts b/mothership/src/memory/search.ts new file mode 100644 index 0000000..9361abd --- /dev/null +++ b/mothership/src/memory/search.ts @@ -0,0 +1,127 @@ +/** + * Hybrid search: FTS5 full-text + sqlite-vec vector similarity. + * Merged via Reciprocal Rank Fusion (RRF) for best-of-both ranking. + */ + +import { getDb } from '../db/index.js'; +import { embed, vectorToBlob } from './embeddings.js'; +import { log } from '../utils/logger.js'; +import type { MemoryEntry } from '../db/schema.js'; + +export interface SearchResult { + entry: MemoryEntry; + score: number; + source: 'fts' | 'vector' | 'hybrid'; +} + +const RRF_K = 60; // Standard RRF constant + +/** Sanitize FTS5 query โ€” strip special operators, wrap terms in quotes */ +function sanitizeFtsQuery(query: string): string { + const terms = query + .replace(/["\-*(){}:^~|<>]/g, ' ') // Strip FTS5 special chars + .split(/\s+/) + .filter(t => t.length > 0 && t.length < 100); // Drop empty and absurdly long terms + if (!terms.length) return '""'; + return terms.map(t => `"${t}"`).join(' '); +} + +/** Full-text search via FTS5 with BM25 ranking */ +export function searchFTS(workspaceId: string, query: string, limit = 20): SearchResult[] { + const db = getDb(); + const safeQuery = sanitizeFtsQuery(query); + const rows = db.prepare(` + SELECT me.id, me.workspace_id, me.session_id, me.event_id, me.content, me.source_type, me.created_at, rank + FROM memory_fts fts + JOIN memory_entries me ON me.rowid = fts.rowid + WHERE memory_fts MATCH ? AND me.workspace_id = ? + ORDER BY rank + LIMIT ? + `).all(safeQuery, workspaceId, limit) as (MemoryEntry & { rank: number })[]; + + return rows.map((r, i) => ({ + entry: r, + score: 1 / (RRF_K + i + 1), + source: 'fts' as const, + })); +} + +/** Vector similarity search via sqlite-vec */ +export function searchVector(workspaceId: string, queryVec: number[], limit = 20): SearchResult[] { + const db = getDb(); + const blob = vectorToBlob(queryVec); + + // sqlite-vec: find nearest neighbors using vec_distance_cosine + // We need memory_entries that have embeddings and match workspace + const rows = db.prepare(` + SELECT me.id, me.workspace_id, me.session_id, me.event_id, me.content, me.source_type, me.created_at, + vec_distance_cosine(me.embedding, ?) as distance + FROM memory_entries me + WHERE me.workspace_id = ? AND me.embedding IS NOT NULL + ORDER BY distance ASC + LIMIT ? + `).all(blob, workspaceId, limit) as (MemoryEntry & { distance: number })[]; + + return rows.map((r, i) => ({ + entry: r, + score: 1 / (RRF_K + i + 1), + source: 'vector' as const, + })); +} + +/** Hybrid search: combine FTS5 + vector results via Reciprocal Rank Fusion */ +export async function hybridSearch( + userId: string, + workspaceId: string, + query: string, + limit = 20, +): Promise { + limit = Math.min(limit, 100); // Cap to prevent abuse + + // Run FTS search (may fail on malformed query syntax) + let ftsResults: SearchResult[] = []; + try { + ftsResults = searchFTS(workspaceId, query, limit); + } catch (err) { + log.debug({ err }, 'FTS search failed, falling back to vector-only'); + } + + // Try vector search (requires embeddings) + let vecResults: SearchResult[] = []; + try { + const embedding = await embed(userId, query); + if (embedding) { + vecResults = searchVector(workspaceId, embedding.embedding, limit); + } + } catch (err) { + log.debug({ err }, 'Vector search skipped'); + } + + // If only one source, return it directly + if (!vecResults.length) return ftsResults.slice(0, limit); + if (!ftsResults.length) return vecResults.slice(0, limit); + + // RRF merge: accumulate scores by entry ID + const scores = new Map(); + + for (const r of ftsResults) { + const prev = scores.get(r.entry.id); + scores.set(r.entry.id, { + entry: r.entry, + score: (prev?.score ?? 0) + r.score, + }); + } + + for (const r of vecResults) { + const prev = scores.get(r.entry.id); + scores.set(r.entry.id, { + entry: prev?.entry ?? r.entry, + score: (prev?.score ?? 0) + r.score, + }); + } + + return [...scores.values()] + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map(({ entry, score }) => ({ entry, score, source: 'hybrid' as const })); +} diff --git a/mothership/src/queue/laneQueue.ts b/mothership/src/queue/laneQueue.ts new file mode 100644 index 0000000..8a9aa9d --- /dev/null +++ b/mothership/src/queue/laneQueue.ts @@ -0,0 +1,30 @@ +/** + * Lane Queue โ€” per-session serial execution. + * Pattern from OpenClaw: prevents race conditions by ensuring + * tasks for the same session execute one at a time. + * + * Key format: "ws:{workspaceId}:sess:{sessionId}" + * Each key chains promises so tasks run sequentially. + */ + +import { log } from '../utils/logger.js'; + +const lanes = new Map>(); + +export function enqueue(key: string, task: () => Promise): Promise { + const prev = lanes.get(key) ?? Promise.resolve(); + const next = prev + .then(task) + .catch((err) => { log.error({ err, lane: key }, 'Lane task failed'); }) + .finally(() => { + // Clean up lane if nothing else queued + if (lanes.get(key) === next) lanes.delete(key); + }); + + lanes.set(key, next); + return next; +} + +export function laneKey(workspaceId: string, sessionId: string) { + return `ws:${workspaceId}:sess:${sessionId}`; +} diff --git a/mothership/src/utils/crypto.ts b/mothership/src/utils/crypto.ts new file mode 100644 index 0000000..1db94fc --- /dev/null +++ b/mothership/src/utils/crypto.ts @@ -0,0 +1,63 @@ +import { randomBytes, scryptSync, timingSafeEqual, createCipheriv, createDecipheriv } from 'node:crypto'; +import { SignJWT, jwtVerify } from 'jose'; +import { config } from '../config.js'; + +const encoder = new TextEncoder(); +const secret = () => encoder.encode(config.jwtSecret); + +// --- Password hashing (scrypt) --- + +export function hashPassword(password: string): string { + const salt = randomBytes(16).toString('hex'); + const hash = scryptSync(password, salt, 64).toString('hex'); + return `${salt}:${hash}`; +} + +export function verifyPassword(password: string, stored: string): boolean { + if (!stored || !stored.includes(':')) return false; + const [salt, hash] = stored.split(':'); + if (!salt || !hash) return false; + try { + const candidate = scryptSync(password, salt, 64); + return timingSafeEqual(candidate, Buffer.from(hash, 'hex')); + } catch { + return false; + } +} + +// --- JWT --- + +export async function signToken(payload: Record, expiresIn = '30d') { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime(expiresIn) + .sign(secret()); +} + +export async function verifyToken(token: string) { + const { payload } = await jwtVerify(token, secret()); + return payload as Record; +} + +// --- AES-256-GCM encryption for secrets at rest --- + +/** Derive a 32-byte encryption key from the JWT secret */ +const encKey = () => scryptSync(config.jwtSecret, 'regent-enc-salt', 32); + +/** Encrypt a plaintext string โ†’ "iv:tag:ciphertext" (all hex) */ +export function encryptSecret(plaintext: string): string { + const iv = randomBytes(12); + const cipher = createCipheriv('aes-256-gcm', encKey(), iv); + const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + return `${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`; +} + +/** Decrypt an "iv:tag:ciphertext" string back to plaintext */ +export function decryptSecret(encrypted: string): string { + const [ivHex, tagHex, encHex] = encrypted.split(':'); + const decipher = createDecipheriv('aes-256-gcm', encKey(), Buffer.from(ivHex, 'hex')); + decipher.setAuthTag(Buffer.from(tagHex, 'hex')); + return decipher.update(Buffer.from(encHex, 'hex'), undefined, 'utf8') + decipher.final('utf8'); +} diff --git a/mothership/src/utils/id.ts b/mothership/src/utils/id.ts new file mode 100644 index 0000000..8b2bce6 --- /dev/null +++ b/mothership/src/utils/id.ts @@ -0,0 +1,3 @@ +import { nanoid } from 'nanoid'; + +export const newId = (size = 21) => nanoid(size); diff --git a/mothership/src/utils/logger.ts b/mothership/src/utils/logger.ts new file mode 100644 index 0000000..1ce64c1 --- /dev/null +++ b/mothership/src/utils/logger.ts @@ -0,0 +1,4 @@ +import pino from 'pino'; +import { config } from '../config.js'; + +export const log = pino({ level: config.logLevel }); diff --git a/mothership/src/utils/rateLimit.ts b/mothership/src/utils/rateLimit.ts new file mode 100644 index 0000000..7fedf72 --- /dev/null +++ b/mothership/src/utils/rateLimit.ts @@ -0,0 +1,42 @@ +/** + * Token bucket rate limiter โ€” per-user, per-bucket. + * Tokens refill over time. Returns true if action is allowed. + * All operations are atomic (wrapped in transaction). + */ + +import { getDb } from '../db/index.js'; + +/** Check and consume rate limit tokens. Returns true if allowed. */ +export function checkRateLimit(userId: string, bucket: string, cost = 1): boolean { + const db = getDb(); + + // Atomic: upsert + refill + consume in a single transaction + return db.transaction(() => { + db.prepare(`INSERT OR IGNORE INTO rate_limits (user_id, bucket) VALUES (?, ?)`).run(userId, bucket); + + const row = db.prepare('SELECT * FROM rate_limits WHERE user_id = ? AND bucket = ?').get(userId, bucket) as { + tokens: number; max_tokens: number; refill_rate: number; last_refill: string; + }; + + const elapsed = (Date.now() - new Date(row.last_refill).getTime()) / 60_000; // minutes + const refilled = Math.min(row.max_tokens, row.tokens + Math.floor(elapsed * row.refill_rate)); + + if (refilled < cost) { + db.prepare("UPDATE rate_limits SET tokens = ?, last_refill = datetime('now') WHERE user_id = ? AND bucket = ?") + .run(refilled, userId, bucket); + return false; + } + + db.prepare("UPDATE rate_limits SET tokens = ?, last_refill = datetime('now') WHERE user_id = ? AND bucket = ?") + .run(refilled - cost, userId, bucket); + return true; + })(); +} + +/** Get current rate limit status */ +export function getRateLimitStatus(userId: string, bucket: string): { tokens: number; maxTokens: number } | null { + const db = getDb(); + const row = db.prepare('SELECT tokens, max_tokens FROM rate_limits WHERE user_id = ? AND bucket = ?') + .get(userId, bucket) as { tokens: number; max_tokens: number } | undefined; + return row ? { tokens: row.tokens, maxTokens: row.max_tokens } : null; +} diff --git a/mothership/src/ws/gateway.ts b/mothership/src/ws/gateway.ts new file mode 100644 index 0000000..e9c4fe2 --- /dev/null +++ b/mothership/src/ws/gateway.ts @@ -0,0 +1,199 @@ +import { WebSocketServer, WebSocket } from 'ws'; +import type { Server } from 'node:http'; +import { verifyToken } from '../utils/crypto.js'; +import { log } from '../utils/logger.js'; +import { bus } from '../events/bus.js'; +import { getDb } from '../db/index.js'; +import { addConnection, removeConnection } from './registry.js'; +import { handleTabRegister } from './handlers/tabRegister.js'; +import { handleEventsStore } from './handlers/eventsStore.js'; +import { handleContextQuery } from './handlers/contextQuery.js'; +import { handleAgentStart, handleAgentStop, cleanupAgentListeners } from './handlers/agentControl.js'; +import { upsertProviderCredentials } from '../memory/embeddings.js'; +import type { Connection } from './registry.js'; + +const HEARTBEAT_INTERVAL = 30_000; +const AUTH_TIMEOUT = 10_000; // 10s to send auth message + +interface WsEnvelope { + type: string; + payload?: unknown; + correlationId?: string; + ts?: number; +} + +function send(ws: WebSocket, msg: WsEnvelope) { + if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)); +} + +export function attachWebSocket(server: Server) { + const wss = new WebSocketServer({ noServer: true }); + + // Accept upgrade without token in URL โ€” auth happens via first message + server.on('upgrade', (req, socket, head) => { + const url = new URL(req.url || '/', `http://${req.headers.host}`); + + // Support both: legacy URL token (for backwards compat) and Sec-WebSocket-Protocol header + const urlToken = url.searchParams.get('token'); + const headerToken = req.headers['sec-websocket-protocol']; + const tabId = url.searchParams.get('tabId') || `tab-${Date.now()}`; + + if (urlToken || headerToken) { + // Authenticate immediately (legacy mode or header mode) + const token = headerToken || urlToken!; + verifyToken(token) + .then(payload => { + const subprotocols = headerToken ? [headerToken] : undefined; + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, { + userId: payload.sub as string, + username: payload.username as string, + tabId, + authenticated: true, + }); + }); + }) + .catch(() => { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + }); + } else { + // First-message auth mode + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, { userId: '', username: '', tabId, authenticated: false }); + }); + } + }); + + wss.on('connection', (ws: WebSocket, meta: { userId: string; username: string; tabId: string; authenticated: boolean }) => { + // If not yet authenticated, wait for auth message + if (!meta.authenticated) { + const authTimer = setTimeout(() => { + send(ws, { type: 'error', payload: { message: 'Auth timeout โ€” send auth message within 10s' } }); + ws.close(4001, 'Auth timeout'); + }, AUTH_TIMEOUT); + + ws.once('message', async (raw) => { + clearTimeout(authTimer); + try { + const msg = JSON.parse(raw.toString()); + if (msg.type !== 'auth' || !msg.payload?.token) { + send(ws, { type: 'error', payload: { message: 'First message must be auth' } }); + ws.close(4002, 'Auth required'); + return; + } + const payload = await verifyToken(msg.payload.token); + + // Token revocation check + const userId = payload.sub as string; + if (payload.iat) { + const db = getDb(); + const user = db.prepare('SELECT updated_at FROM users WHERE id = ?').get(userId) as { updated_at: string } | undefined; + if (user && (payload.iat as number) * 1000 < new Date(user.updated_at).getTime()) { + send(ws, { type: 'error', payload: { message: 'Token revoked' } }); + ws.close(4001, 'Token revoked'); + return; + } + } + + meta.userId = userId; + meta.username = payload.username as string; + meta.tabId = msg.payload.tabId || meta.tabId; + meta.authenticated = true; + setupConnection(ws, meta); + } catch { + send(ws, { type: 'error', payload: { message: 'Invalid token' } }); + ws.close(4001, 'Invalid token'); + } + }); + return; + } + + setupConnection(ws, meta); + }); + + function setupConnection(ws: WebSocket, meta: { userId: string; username: string; tabId: string }) { + const conn: Connection = { ws, userId: meta.userId, tabId: meta.tabId, workspaceId: null }; + addConnection(conn); + log.info({ userId: meta.userId, tabId: meta.tabId }, 'WS connected'); + + send(ws, { type: 'connected', payload: { userId: meta.userId, username: meta.username } }); + + // Heartbeat + let alive = true; + ws.on('pong', () => { alive = true; }); + const heartbeat = setInterval(() => { + if (!alive) { ws.terminate(); return; } + alive = false; + ws.ping(); + }, HEARTBEAT_INTERVAL); + + // Listen for bus events โ†’ forward to this connection + const onNewEvents = (data: { workspaceId: string; sessionId: string; events: unknown[]; sourceTabId: string }) => { + if (conn.workspaceId === data.workspaceId && conn.tabId !== data.sourceTabId) { + send(ws, { type: 'events:cross', payload: { sessionId: data.sessionId, events: data.events } }); + } + }; + bus.on('events:new', onNewEvents); + + // Listen for notifications targeted at this user + const onNotification = (data: { userId: string; notification: unknown }) => { + if (data.userId === conn.userId) { + send(ws, { type: 'notification', payload: data.notification }); + } + }; + bus.on('notification:new', onNotification); + + // Message dispatch + ws.on('message', (raw) => { + try { + const msg = JSON.parse(raw.toString()) as WsEnvelope; + switch (msg.type) { + case 'tab:register': + handleTabRegister(conn, msg.payload as { workspaceId: string }); + break; + case 'events:store': + handleEventsStore(conn, msg.payload as any); + break; + case 'context:query': + handleContextQuery(conn, { ...(msg.payload as any), correlationId: msg.correlationId }, send); + break; + case 'provider:credentials': { + const creds = msg.payload as any; + if (creds?.provider && creds?.apiKey) { + upsertProviderCredentials(conn.userId, creds); + send(ws, { type: 'provider:ack', correlationId: msg.correlationId }); + } else { + send(ws, { type: 'error', payload: { message: 'provider and apiKey required' }, correlationId: msg.correlationId }); + } + break; + } + case 'agent:start': + handleAgentStart(conn, msg.payload as any, send); + break; + case 'agent:stop': + handleAgentStop(conn, msg.payload as any, send); + break; + case 'ping': + send(ws, { type: 'pong', ts: Date.now() }); + break; + default: + send(ws, { type: 'error', payload: { message: `Unknown type: ${msg.type}` } }); + } + } catch (err) { + log.warn({ err }, 'Invalid WS message'); + } + }); + + ws.on('close', () => { + clearInterval(heartbeat); + bus.off('events:new', onNewEvents); + bus.off('notification:new', onNotification); + cleanupAgentListeners(conn); + removeConnection(meta.userId, meta.tabId); + log.info({ userId: meta.userId, tabId: meta.tabId }, 'WS disconnected'); + }); + } + + return wss; +} diff --git a/mothership/src/ws/handlers/agentControl.ts b/mothership/src/ws/handlers/agentControl.ts new file mode 100644 index 0000000..e1e7fcc --- /dev/null +++ b/mothership/src/ws/handlers/agentControl.ts @@ -0,0 +1,124 @@ +/** + * WebSocket handlers for agent control โ€” start/stop agent runs, stream results. + */ + +import { getAgent } from '../../agents/manager.js'; +import { startRun, cancelRun, getRun } from '../../agents/runtime.js'; +import { bus } from '../../events/bus.js'; +import { log } from '../../utils/logger.js'; +import type { Connection } from '../registry.js'; +import type { WebSocket } from 'ws'; + +interface AgentStartPayload { agentId: string; input: string; sessionId?: string; } +interface AgentStopPayload { runId: string; } + +type SendFn = (ws: WebSocket, msg: any) => void; + +/** Track bus listeners per connection for cleanup on disconnect */ +const connectionListeners = new Map void }>>(); + +function connKey(conn: Connection) { return `${conn.userId}:${conn.tabId}`; } + +function trackListener(conn: Connection, event: string, fn: (...args: any[]) => void) { + const key = connKey(conn); + if (!connectionListeners.has(key)) connectionListeners.set(key, []); + connectionListeners.get(key)!.push({ event, fn }); + bus.on(event, fn); +} + +function untrackListener(conn: Connection, fn: (...args: any[]) => void) { + const key = connKey(conn); + const arr = connectionListeners.get(key); + if (!arr) return; + const idx = arr.findIndex(l => l.fn === fn); + if (idx !== -1) { + bus.off(arr[idx].event, arr[idx].fn); + arr.splice(idx, 1); + } +} + +/** Clean up all agent bus listeners for a connection โ€” call from gateway on WS close */ +export function cleanupAgentListeners(conn: Connection) { + const key = connKey(conn); + const listeners = connectionListeners.get(key); + if (!listeners) return; + for (const { event, fn } of listeners) bus.off(event, fn); + connectionListeners.delete(key); +} + +export function handleAgentStart(conn: Connection, payload: AgentStartPayload, send: SendFn) { + const { workspaceId, userId } = conn; + if (!workspaceId) return; + + const { agentId, input } = payload; + if (!agentId || !input) { + send(conn.ws, { type: 'error', payload: { message: 'agentId and input required' } }); + return; + } + + const agent = getAgent(agentId); + if (!agent || agent.workspace_id !== workspaceId) { + send(conn.ws, { type: 'error', payload: { message: 'Agent not found' } }); + return; + } + + startRun({ agent, userId, input, sessionId: payload.sessionId }) + .then(runId => { + send(conn.ws, { type: 'agent:started', payload: { runId, agentId } }); + + let safetyTimer: ReturnType | null = null; + + const cleanup = () => { + // Clear the safety timeout + if (safetyTimer) { clearTimeout(safetyTimer); safetyTimer = null; } + // Remove from both bus AND connectionListeners tracking + untrackListener(conn, onStream); + untrackListener(conn, onToolCall); + untrackListener(conn, onError); + }; + + const onStream = (data: any) => { + if (data.runId !== runId) return; + send(conn.ws, { type: 'agent:stream', payload: data }); + if (data.done) cleanup(); + }; + const onToolCall = (data: any) => { + if (data.runId !== runId) return; + send(conn.ws, { type: 'agent:tool_call', payload: data }); + }; + const onError = (data: any) => { + if (data.runId !== runId) return; + send(conn.ws, { type: 'agent:error', payload: data }); + cleanup(); + }; + + trackListener(conn, 'agent:stream', onStream); + trackListener(conn, 'agent:tool_call', onToolCall); + trackListener(conn, 'agent:error', onError); + + // Safety net: auto-cleanup after 5 minutes (clearable) + safetyTimer = setTimeout(cleanup, 5 * 60 * 1000); + }) + .catch(err => { + log.warn({ err, agentId }, 'Agent start failed'); + send(conn.ws, { type: 'agent:error', payload: { error: err.message } }); + }); +} + +export function handleAgentStop(conn: Connection, payload: AgentStopPayload, send: SendFn) { + const { runId } = payload; + if (!runId) { + send(conn.ws, { type: 'error', payload: { message: 'runId required' } }); + return; + } + + // Verify run belongs to this user's workspace + const run = getRun(runId); + if (!run || run.workspace_id !== conn.workspaceId) { + send(conn.ws, { type: 'error', payload: { message: 'Run not found' } }); + return; + } + + const ok = cancelRun(runId); + send(conn.ws, { type: 'agent:stopped', payload: { runId, ok } }); +} diff --git a/mothership/src/ws/handlers/contextQuery.ts b/mothership/src/ws/handlers/contextQuery.ts new file mode 100644 index 0000000..6420980 --- /dev/null +++ b/mothership/src/ws/handlers/contextQuery.ts @@ -0,0 +1,47 @@ +/** + * Handle context:query โ€” search memory from extension via WebSocket. + * Returns hybrid search results (FTS5 + vector). + */ + +import { hybridSearch } from '../../memory/search.js'; +import { log } from '../../utils/logger.js'; +import type { Connection } from '../registry.js'; +import type { WebSocket } from 'ws'; + +interface QueryPayload { + query: string; + limit?: number; + correlationId?: string; +} + +export async function handleContextQuery(conn: Connection, payload: QueryPayload, send: (ws: WebSocket, msg: any) => void) { + const { workspaceId, userId } = conn; + if (!workspaceId) return; + + const { query, limit, correlationId } = payload; + if (!query) { + send(conn.ws, { type: 'error', payload: { message: 'query required' }, correlationId }); + return; + } + + try { + const results = await hybridSearch(userId, workspaceId, query, limit || 20); + + send(conn.ws, { + type: 'context:results', + payload: results.map(r => ({ + id: r.entry.id, + content: r.entry.content, + source_type: r.entry.source_type, + session_id: r.entry.session_id, + event_id: r.entry.event_id, + created_at: r.entry.created_at, + score: r.score, + })), + correlationId, + }); + } catch (err) { + log.warn({ err, userId }, 'Context query failed'); + send(conn.ws, { type: 'error', payload: { message: 'Search failed' }, correlationId }); + } +} diff --git a/mothership/src/ws/handlers/eventsStore.ts b/mothership/src/ws/handlers/eventsStore.ts new file mode 100644 index 0000000..3e14ee3 --- /dev/null +++ b/mothership/src/ws/handlers/eventsStore.ts @@ -0,0 +1,102 @@ +import { getDb } from '../../db/index.js'; +import { newId } from '../../utils/id.js'; +import { bus } from '../../events/bus.js'; +import { enqueue, laneKey } from '../../queue/laneQueue.js'; +import { embed, vectorToBlob } from '../../memory/embeddings.js'; +import { log } from '../../utils/logger.js'; +import type { Connection } from '../registry.js'; +import type { RegentEvent } from '../../db/schema.js'; + +interface EventPayload { + sessionId: string; + sessionName?: string; + url?: string; + hostname?: string; + events: Array<{ + title: string; + summary: string; + importance?: string; + messageIndex?: number; + }>; +} + +/** + * Handle events:store โ€” extension forwards pre-extracted events. + * Uses lane queue for per-session serial execution. + */ +export function handleEventsStore(conn: Connection, payload: EventPayload) { + const { workspaceId } = conn; + if (!workspaceId) return; + + const { sessionId, events } = payload; + if (!sessionId || !events?.length) return; + + const key = laneKey(workspaceId, sessionId); + + enqueue(key, async () => { + const db = getDb(); + + const insert = db.prepare(`INSERT INTO events (id, session_id, workspace_id, title, summary, importance, message_index, source_tab_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`); + + // Atomic: upsert session + store all events in one transaction + const stored: RegentEvent[] = []; + db.transaction(() => { + const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId); + if (!existing) { + db.prepare(`INSERT INTO sessions (id, workspace_id, name, url, hostname, status) + VALUES (?, ?, ?, ?, ?, 'active')`) + .run(sessionId, workspaceId, payload.sessionName ?? null, payload.url ?? null, payload.hostname ?? null); + } else { + db.prepare("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?").run(sessionId); + } + + for (const evt of events) { + const id = newId(); + insert.run(id, sessionId, workspaceId, evt.title, evt.summary, evt.importance || 'medium', evt.messageIndex ?? null, conn.tabId); + stored.push({ + id, session_id: sessionId, workspace_id: workspaceId, + title: evt.title, summary: evt.summary, + importance: (evt.importance || 'medium') as RegentEvent['importance'], + message_index: evt.messageIndex ?? null, + source_tab_id: conn.tabId, created_at: new Date().toISOString(), + }); + } + })(); + + log.info({ sessionId, count: stored.length }, 'Events stored'); + + // Broadcast to other tabs in workspace + bus.emit('events:new', { workspaceId, sessionId, events: stored, sourceTabId: conn.tabId }); + + // Auto-embed events into memory_entries (fire-and-forget, non-blocking) + autoEmbed(conn.userId, workspaceId, sessionId, stored).catch((err) => { + log.warn({ err, sessionId }, 'Auto-embed failed'); + }); + }); +} + +/** Create memory entries with embeddings for stored events */ +async function autoEmbed(userId: string, workspaceId: string, sessionId: string, events: RegentEvent[]) { + const db = getDb(); + const insert = db.prepare(`INSERT INTO memory_entries (id, workspace_id, session_id, event_id, content, embedding, source_type) + VALUES (?, ?, ?, ?, ?, ?, 'event')`); + + for (const evt of events) { + const content = `${evt.title}: ${evt.summary}`; + let embeddingBlob: Buffer | null = null; + + try { + const result = await embed(userId, content); + if (result) embeddingBlob = vectorToBlob(result.embedding); + } catch (err) { + log.debug({ err, eventId: evt.id }, 'Embedding failed'); + } + + try { + insert.run(newId(), workspaceId, sessionId, evt.id, content, embeddingBlob); + } catch (err) { + log.debug({ err, eventId: evt.id }, 'Memory entry insert failed'); + } + } +} diff --git a/mothership/src/ws/handlers/tabRegister.ts b/mothership/src/ws/handlers/tabRegister.ts new file mode 100644 index 0000000..9af710f --- /dev/null +++ b/mothership/src/ws/handlers/tabRegister.ts @@ -0,0 +1,18 @@ +import { getDb } from '../../db/index.js'; +import type { Connection } from '../registry.js'; + +/** + * Handle tab:register โ€” extension tab announces itself and its workspace. + * Verifies the user is a member of the workspace before allowing registration. + */ +export function handleTabRegister(conn: Connection, payload: { workspaceId: string }) { + const { workspaceId } = payload; + if (!workspaceId) return; + + const db = getDb(); + const member = db.prepare('SELECT 1 FROM workspace_members WHERE workspace_id = ? AND user_id = ?') + .get(workspaceId, conn.userId); + if (!member) return; + + conn.workspaceId = workspaceId; +} diff --git a/mothership/src/ws/registry.ts b/mothership/src/ws/registry.ts new file mode 100644 index 0000000..d2324c0 --- /dev/null +++ b/mothership/src/ws/registry.ts @@ -0,0 +1,52 @@ +import type { WebSocket } from 'ws'; + +export interface Connection { + ws: WebSocket; + userId: string; + tabId: string; + workspaceId: string | null; +} + +/** userId โ†’ Map */ +const connections = new Map>(); + +export function addConnection(conn: Connection) { + let userConns = connections.get(conn.userId); + if (!userConns) { + userConns = new Map(); + connections.set(conn.userId, userConns); + } + userConns.set(conn.tabId, conn); +} + +export function removeConnection(userId: string, tabId: string) { + const userConns = connections.get(userId); + if (!userConns) return; + userConns.delete(tabId); + if (userConns.size === 0) connections.delete(userId); +} + +export function getConnection(userId: string, tabId: string): Connection | undefined { + return connections.get(userId)?.get(tabId); +} + +/** Get all connections for a workspace (across all users) */ +export function getWorkspaceConnections(workspaceId: string): Connection[] { + const result: Connection[] = []; + for (const userConns of connections.values()) { + for (const conn of userConns.values()) { + if (conn.workspaceId === workspaceId) result.push(conn); + } + } + return result; +} + +/** Broadcast a message to all tabs in a workspace, optionally excluding a tabId */ +export function broadcastToWorkspace(workspaceId: string, message: object, excludeTabId?: string) { + const payload = JSON.stringify(message); + for (const conn of getWorkspaceConnections(workspaceId)) { + if (conn.tabId !== excludeTabId && conn.ws.readyState === 1) { + try { conn.ws.send(payload); } catch { /* connection already closing */ } + } + } +} diff --git a/mothership/tsconfig.json b/mothership/tsconfig.json new file mode 100644 index 0000000..55fc5c4 --- /dev/null +++ b/mothership/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "typeRoots": ["./node_modules/@types"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "data"] +} diff --git a/src/background.js b/src/background.js index b9bf6b0..903d678 100644 --- a/src/background.js +++ b/src/background.js @@ -319,6 +319,132 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } }); +// โ”€โ”€โ”€ Mothership WebSocket Connection Manager โ”€โ”€โ”€ + +let mothershipWs = null; +let mothershipReconnectTimer = null; +let mothershipReconnectDelay = 1000; +const MOTHERSHIP_MAX_RECONNECT = 30000; + +function mothershipConnect(url, token, tabId) { + mothershipDisconnect(); + + // Connect without token in URL โ€” authenticate via first message + const wsUrl = `${url.replace(/^http/, 'ws')}/ws`; + mothershipWs = new WebSocket(wsUrl); + + mothershipWs.onopen = () => { + mothershipReconnectDelay = 1000; + // Authenticate via first message (not URL query) + if (mothershipWs?.readyState === WebSocket.OPEN) { + mothershipWs.send(JSON.stringify({ type: 'auth', payload: { token, tabId: tabId || `tab-${Date.now()}` } })); + } + // Register with workspace (from local) + forward provider credentials (from sync) + chrome.storage.local.get(['mothershipWorkspaceId'], (localData) => { + if (localData.mothershipWorkspaceId && mothershipWs?.readyState === WebSocket.OPEN) { + mothershipWs.send(JSON.stringify({ type: 'tab:register', payload: { workspaceId: localData.mothershipWorkspaceId } })); + chrome.storage.sync.get(['provider', 'deepseekApiKey', 'siliconflowApiKey', 'openrouterApiKey'], (syncData) => { + const provider = syncData.provider || 'deepseek'; + const apiKey = syncData[`${provider}ApiKey`] || ''; + if (apiKey && mothershipWs?.readyState === WebSocket.OPEN) { + mothershipWs.send(JSON.stringify({ type: 'provider:credentials', payload: { provider, apiKey } })); + } + }); + } + }); + broadcastMothershipStatus('connected'); + }; + + mothershipWs.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + // Forward relevant WS messages to all tabs running regent + if (['events:cross', 'connected', 'context:results', 'agent:started', 'agent:stream', 'agent:tool_call', 'agent:error', 'agent:stopped', 'notification'].includes(msg.type)) { + chrome.tabs.query({}, (tabs) => { + for (const tab of tabs) { + chrome.tabs.sendMessage(tab.id, { type: 'mothershipEvent', data: msg }).catch(() => {}); + } + }); + } + } catch (err) { + console.warn('Mothership WS message parse error:', err); + } + }; + + mothershipWs.onclose = () => { + mothershipWs = null; + broadcastMothershipStatus('disconnected'); + // Exponential backoff reconnect + mothershipReconnectTimer = setTimeout(() => { + chrome.storage.local.get(['mothershipUrl', 'mothershipToken'], (data) => { + if (data.mothershipUrl && data.mothershipToken) { + mothershipConnect(data.mothershipUrl, data.mothershipToken); + } + }); + }, mothershipReconnectDelay); + mothershipReconnectDelay = Math.min(mothershipReconnectDelay * 2, MOTHERSHIP_MAX_RECONNECT); + }; + + mothershipWs.onerror = () => {}; // onclose handles reconnect +} + +function mothershipDisconnect() { + clearTimeout(mothershipReconnectTimer); + mothershipReconnectTimer = null; + if (mothershipWs) { + mothershipWs.onclose = null; // Prevent reconnect + mothershipWs.close(); + mothershipWs = null; + } + broadcastMothershipStatus('disconnected'); +} + +function mothershipSend(payload) { + if (mothershipWs?.readyState === WebSocket.OPEN) { + mothershipWs.send(JSON.stringify(payload)); + return true; + } + return false; +} + +function broadcastMothershipStatus(status) { + chrome.tabs.query({}, (tabs) => { + for (const tab of tabs) { + chrome.tabs.sendMessage(tab.id, { type: 'mothershipStatus', status }).catch(() => {}); + } + }); +} + +// Handle mothership messages from content scripts +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'mothershipConnect') { + mothershipConnect(request.url, request.token, sender?.tab?.id); + sendResponse({ ok: true }); + return true; + } + if (request.action === 'mothershipDisconnect') { + mothershipDisconnect(); + sendResponse({ ok: true }); + return true; + } + if (request.action === 'mothershipSend') { + const sent = mothershipSend(request.payload); + sendResponse({ sent }); + return true; + } + if (request.action === 'mothershipStatus') { + sendResponse({ connected: mothershipWs?.readyState === WebSocket.OPEN }); + return true; + } +}); + +// Auto-connect on service worker startup if credentials are stored +chrome.storage.local.get(['mothershipUrl', 'mothershipToken'], (data) => { + if (data.mothershipUrl && data.mothershipToken) { + mothershipConnect(data.mothershipUrl, data.mothershipToken); + } +}); + // Create context menu on extension installation chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ diff --git a/src/content/regent/RegentOrchestrator.js b/src/content/regent/RegentOrchestrator.js index 4e86e3f..86861af 100644 --- a/src/content/regent/RegentOrchestrator.js +++ b/src/content/regent/RegentOrchestrator.js @@ -69,6 +69,18 @@ class RegentOrchestratorClass { // Periodic meta-summary this._metaTimer = setInterval(() => this._generateMetaSummary(), META_SUMMARY_INTERVAL); + + // Listen for mothership events (cross-session from other tabs/devices) + this._mothershipListener = (msg) => { + if (msg.type === 'mothershipEvent') this._onMothershipEvent(msg.data); + if (msg.type === 'mothershipStatus') this.sidebar.setConnectionStatus(msg.status); + }; + chrome.runtime.onMessage.addListener(this._mothershipListener); + + // Check initial mothership status + chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => { + this.sidebar.setConnectionStatus(res?.connected ? 'connected' : 'disconnected'); + }); } /** Create a sidecar for a session */ @@ -178,9 +190,39 @@ class RegentOrchestratorClass { this.sidebar.updateMeta(meta); } + /** Handle messages from mothership */ + _onMothershipEvent(data) { + switch (data.type) { + case 'events:cross': { + const { sessionId, events } = data.payload || {}; + if (sessionId && events?.length) this.sidebar.addCrossSessionEvents(sessionId, events); + break; + } + case 'context:results': + this.sidebar.showSearchResults(data.payload); + break; + case 'agent:started': + this.sidebar.handleAgentStarted(data.payload); + break; + case 'agent:stream': + this.sidebar.handleAgentStream(data.payload); + break; + case 'agent:tool_call': + this.sidebar.handleAgentToolCall(data.payload); + break; + case 'agent:error': + this.sidebar.handleAgentError(data.payload); + break; + case 'notification': + this.sidebar.showNotification(data.payload); + break; + } + } + /** Destroy the entire regent system */ destroy() { clearInterval(this._metaTimer); + if (this._mothershipListener) chrome.runtime.onMessage.removeListener(this._mothershipListener); this.detector.destroy(); this.sidecars.forEach(s => s.destroy()); this.sidecars.clear(); diff --git a/src/content/regent/RegentSidebar.js b/src/content/regent/RegentSidebar.js index eb152a1..ae39f01 100644 --- a/src/content/regent/RegentSidebar.js +++ b/src/content/regent/RegentSidebar.js @@ -46,11 +46,26 @@ export class RegentSidebar {
Regent + 0 sessions
โ–ถ
+ + + +
@@ -65,6 +80,41 @@ export class RegentSidebar { // Cache references this.sessionsContainer = this.sidebar.querySelector('.regent-sessions'); this.metaEl = this.sidebar.querySelector('.regent-meta'); + this._searchEl = this.sidebar.querySelector('.regent-search'); + this._searchInput = this.sidebar.querySelector('.regent-search-input'); + this._searchResultsEl = this.sidebar.querySelector('.regent-search-results'); + this._searchDebounce = null; + this._agentPanel = this.sidebar.querySelector('.regent-agent-panel'); + this._agentSelect = this.sidebar.querySelector('.regent-agent-select'); + this._agentInput = this.sidebar.querySelector('.regent-agent-input'); + this._agentOutput = this.sidebar.querySelector('.regent-agent-output'); + this._notificationsEl = this.sidebar.querySelector('.regent-notifications'); + this._currentRunId = null; + + // Agent run button โ€” includes selected agentId + this.sidebar.querySelector('.regent-agent-run-btn').addEventListener('click', () => { + const agentId = this._agentSelect.value; + const task = this._agentInput.value.trim(); + if (!agentId || !task) return; + this._agentOutput.textContent = 'Starting agent...'; + chrome.runtime.sendMessage({ + action: 'mothershipSend', + payload: { type: 'agent:start', payload: { agentId, input: task } }, + }).catch(() => {}); + }); + + // Search input โ€” debounced query via WS + this._searchInput.addEventListener('input', () => { + clearTimeout(this._searchDebounce); + const q = this._searchInput.value.trim(); + if (!q) { this._hideSearchResults(); return; } + this._searchDebounce = setTimeout(() => { + chrome.runtime.sendMessage({ + action: 'mothershipSend', + payload: { type: 'context:query', payload: { query: q, limit: 15 } }, + }).catch(() => {}); + }, 400); + }); // Collapse/expand const collapseBtn = this.sidebar.querySelector('.regent-collapse-btn'); @@ -265,6 +315,177 @@ export class RegentSidebar { this.sessionsContainer.appendChild(cal); } + /** Set mothership connection status indicator */ + setConnectionStatus(status) { + const dot = this.sidebar?.querySelector('.regent-connection-dot'); + if (!dot) return; + const connected = status === 'connected'; + dot.classList.toggle('connected', connected); + dot.title = `Mothership: ${connected ? 'connected' : 'disconnected'}`; + // Show/hide search bar and agent panel based on connection + if (this._searchEl) this._searchEl.style.display = connected ? '' : 'none'; + if (this._agentPanel) this._agentPanel.style.display = connected ? '' : 'none'; + if (this._notificationsEl) this._notificationsEl.style.display = connected ? '' : 'none'; + if (!connected) { + this._hideSearchResults(); + if (this._agentOutput) this._agentOutput.textContent = ''; + } + // Fetch agents list when connected + if (connected) this._loadAgents(); + } + + /** Fetch agents from mothership and populate the selector */ + _loadAgents() { + chrome.storage.local.get(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId'], async (data) => { + if (!data.mothershipUrl || !data.mothershipToken || !data.mothershipWorkspaceId) return; + try { + const res = await fetch(`${data.mothershipUrl}/api/v1/workspaces/${data.mothershipWorkspaceId}/agents`, { + headers: { Authorization: `Bearer ${data.mothershipToken}` }, + }); + if (!res.ok) return; + const agents = await res.json(); + if (!this._agentSelect) return; + this._agentSelect.innerHTML = ''; + for (const a of agents) { + const opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.name; + this._agentSelect.appendChild(opt); + } + } catch {} + }); + } + + /** Show a notification toast in the sidebar */ + showNotification(notification) { + if (!this._notificationsEl) return; + const toast = document.createElement('div'); + toast.className = 'regent-notification-toast'; + toast.innerHTML = ` +
${this._escapeHtml(notification.title)}
+ ${notification.body ? `
${this._escapeHtml(notification.body)}
` : ''} + `; + this._notificationsEl.appendChild(toast); + // Auto-remove after 8 seconds + setTimeout(() => toast.remove(), 8000); + } + + /** Handle agent streaming chunks */ + handleAgentStream(data) { + if (!this._agentOutput) return; + if (data.done) { + this._agentOutput.textContent += '\n--- Done ---'; + this._currentRunId = null; + return; + } + if (data.chunk) { + if (this._agentOutput.textContent === 'Starting agent...') this._agentOutput.textContent = ''; + this._agentOutput.textContent += data.chunk; + } + } + + /** Handle agent started confirmation */ + handleAgentStarted(data) { + this._currentRunId = data.runId; + if (this._agentOutput) this._agentOutput.textContent = ''; + } + + /** Handle agent tool call visualization */ + handleAgentToolCall(data) { + if (!this._agentOutput) return; + const el = document.createElement('div'); + el.className = 'regent-agent-tool-call'; + el.textContent = `Tool: ${data.tool}`; + this._agentOutput.appendChild(el); + } + + /** Handle agent error */ + handleAgentError(data) { + if (!this._agentOutput) return; + this._agentOutput.textContent += `\nError: ${data.error}`; + this._currentRunId = null; + } + + /** Display search results from mothership context:results */ + showSearchResults(results) { + if (!results?.length) { + this._searchResultsEl.innerHTML = '
No results found
'; + this._searchResultsEl.style.display = ''; + return; + } + + this._searchResultsEl.innerHTML = ''; + for (const r of results) { + const el = document.createElement('div'); + el.className = 'regent-search-result'; + const time = r.created_at ? new Date(r.created_at).toLocaleDateString([], { month: 'short', day: 'numeric' }) : ''; + el.innerHTML = ` +
${this._escapeHtml(r.content.slice(0, 120))}
+
+ ${r.source_type || 'event'} + ${time} +
+ `; + this._searchResultsEl.appendChild(el); + } + this._searchResultsEl.style.display = ''; + } + + /** Hide search results panel */ + _hideSearchResults() { + if (this._searchResultsEl) { + this._searchResultsEl.style.display = 'none'; + this._searchResultsEl.innerHTML = ''; + } + } + + /** Display cross-session events received from mothership (other tabs/devices) */ + addCrossSessionEvents(sessionId, events) { + if (!events?.length) return; + + // Get or create a "remote" session section + let section = this._sessionElements.get(`remote:${sessionId}`); + if (!section) { + const empty = this.sessionsContainer?.querySelector('.regent-empty'); + if (empty) empty.remove(); + + section = document.createElement('div'); + section.className = 'regent-session regent-session-remote'; + section.dataset.sessionId = `remote:${sessionId}`; + section.innerHTML = ` +
+ Remote: ${this._escapeHtml(sessionId.slice(-8))} + Remote +
+
+ `; + this.sessionsContainer?.appendChild(section); + this._sessionElements.set(`remote:${sessionId}`, section); + } + + const container = section.querySelector('.regent-events'); + for (const evt of events) { + const el = document.createElement('div'); + el.className = 'regent-event entering'; + el.dataset.importance = evt.importance || 'medium'; + const time = new Date(evt.created_at || Date.now()).toLocaleTimeString([], { + hour: '2-digit', minute: '2-digit', + }); + el.innerHTML = ` +
+
+ ${this._escapeHtml(evt.title)} + ${time} +
+
${this._escapeHtml(evt.summary)}
+
+ `; + container.appendChild(el); + } + + this._updateBadge(); + } + /** Update meta-summary */ updateMeta(text) { if (!text) { diff --git a/src/content/regent/RegentSidecar.js b/src/content/regent/RegentSidecar.js index 2b8cd2c..e9e8a4a 100644 --- a/src/content/regent/RegentSidecar.js +++ b/src/content/regent/RegentSidecar.js @@ -92,6 +92,9 @@ export class RegentSidecar { this._processedCount += batch.length; this.onEventsUpdate?.(this.sessionId, this.events); + + // Forward extracted events to mothership (fire-and-forget) + this._forwardToMothership(aiEvents); } catch (err) { console.warn(`[Regent:Sidecar:${this.sessionId}] Summarization failed:`, err.message); // Drop failed batch to avoid infinite retry loop โ€” messages are lost but system stays stable @@ -108,6 +111,28 @@ export class RegentSidecar { } } + /** Forward extracted events to mothership via background WS */ + _forwardToMothership(aiEvents) { + if (!aiEvents?.length) return; + chrome.runtime.sendMessage({ + action: 'mothershipSend', + payload: { + type: 'events:store', + payload: { + sessionId: this.sessionId, + sessionName: this.getDisplayName(), + url: location.href, + hostname: location.hostname, + events: aiEvents.map(e => ({ + title: e.title, summary: e.summary, + importance: e.importance || 'medium', + messageIndex: e.messageIndex, + })), + }, + }, + }).catch(() => {}); // Silent fail โ€” mothership is optional + } + /** Get session display name */ getDisplayName() { // Try to extract from URL or element content diff --git a/src/content/regent/regent.css b/src/content/regent/regent.css index 2091cb9..ade443e 100644 --- a/src/content/regent/regent.css +++ b/src/content/regent/regent.css @@ -111,6 +111,26 @@ letter-spacing: -0.01em; } +.regent-connection-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--regent-low); + display: inline-block; + margin: 0 4px; + transition: background 0.3s; +} + +.regent-connection-dot.connected { + background: #34c759; + box-shadow: 0 0 6px rgba(52, 199, 89, 0.4); +} + +.regent-session-remote .session-status.remote { + background: rgba(88, 86, 214, 0.12); + color: #5856d6; +} + .regent-badge { font-size: 11px; font-weight: 500; @@ -366,6 +386,186 @@ .regent-calibration-btn:disabled { opacity: 0.6; cursor: wait; } .regent-calibration-btn + .regent-calibration-btn { margin-top: 8px; } +/* โ”€โ”€โ”€ Memory search โ”€โ”€โ”€ */ +.regent-search { + padding: 8px 16px; + border-bottom: 1px solid var(--regent-border); + flex-shrink: 0; +} +.regent-search-input { + width: 100%; + padding: 6px 10px; + border: 1px solid var(--regent-border); + border-radius: 8px; + background: var(--regent-event-bg); + color: var(--regent-text); + font-size: 12px; + outline: none; + transition: border-color 0.2s; + box-sizing: border-box; + font-family: inherit; +} +.regent-search-input:focus { + border-color: var(--regent-accent); +} +.regent-search-input::placeholder { + color: var(--regent-text-secondary); +} + +.regent-search-results { + max-height: 240px; + overflow-y: auto; + border-bottom: 1px solid var(--regent-border); + scrollbar-width: thin; + scrollbar-color: var(--regent-border) transparent; +} + +.regent-search-result { + padding: 8px 16px; + border-bottom: 1px solid var(--regent-border); + cursor: default; +} +.regent-search-result:last-child { border-bottom: none; } + +.search-result-content { + font-size: 12px; + color: var(--regent-text); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.search-result-meta { + display: flex; + gap: 8px; + margin-top: 4px; + font-size: 10px; + color: var(--regent-text-secondary); +} + +.search-result-type { + background: var(--regent-accent-bg); + color: var(--regent-accent); + padding: 0 5px; + border-radius: 4px; + font-weight: 500; +} + +.regent-search-empty { + padding: 16px; + text-align: center; + font-size: 12px; + color: var(--regent-text-secondary); +} + +/* โ”€โ”€โ”€ Agent panel โ”€โ”€โ”€ */ +.regent-agent-panel { + padding: 8px 16px; + border-bottom: 1px solid var(--regent-border); + flex-shrink: 0; +} +.regent-agent-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} +.regent-agent-title { + font-size: 12px; + font-weight: 600; + color: var(--regent-text); +} +.regent-agent-run-btn { + background: var(--regent-accent); + color: #fff; + border: none; + border-radius: 6px; + padding: 3px 10px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: opacity 0.15s; +} +.regent-agent-run-btn:hover { opacity: 0.85; } +.regent-agent-input { + width: 100%; + padding: 6px 10px; + border: 1px solid var(--regent-border); + border-radius: 8px; + background: var(--regent-event-bg); + color: var(--regent-text); + font-size: 12px; + outline: none; + box-sizing: border-box; + font-family: inherit; + margin-bottom: 6px; +} +.regent-agent-input:focus { border-color: var(--regent-accent); } +.regent-agent-input::placeholder { color: var(--regent-text-secondary); } +.regent-agent-output { + max-height: 200px; + overflow-y: auto; + font-size: 11px; + color: var(--regent-text-secondary); + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + scrollbar-width: thin; + scrollbar-color: var(--regent-border) transparent; +} +.regent-agent-select { + flex: 1; + min-width: 0; + padding: 3px 6px; + border: 1px solid var(--regent-border); + border-radius: 6px; + background: var(--regent-event-bg); + color: var(--regent-text); + font-size: 11px; + font-family: inherit; + outline: none; + margin: 0 6px; +} +.regent-agent-select:focus { border-color: var(--regent-accent); } +.regent-agent-tool-call { + padding: 2px 6px; + margin: 4px 0; + background: var(--regent-accent-bg); + color: var(--regent-accent); + border-radius: 4px; + font-size: 10px; + font-weight: 500; +} + +/* โ”€โ”€โ”€ Notifications โ”€โ”€โ”€ */ +.regent-notifications { + padding: 0 12px; +} +.regent-notification-toast { + padding: 8px 12px; + margin: 4px 0; + background: var(--regent-accent-bg); + border-left: 3px solid var(--regent-accent); + border-radius: 6px; + animation: regent-toast-in 0.3s ease-out; +} +.regent-notification-toast .notification-title { + font-size: 12px; + font-weight: 600; + color: var(--regent-text); +} +.regent-notification-toast .notification-body { + font-size: 11px; + color: var(--regent-text-secondary); + margin-top: 2px; +} +@keyframes regent-toast-in { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } +} + /* โ”€โ”€โ”€ Scrollbar smooth โ”€โ”€โ”€ */ .regent-sessions { scroll-behavior: smooth; diff --git a/src/popup/MothershipManager.js b/src/popup/MothershipManager.js new file mode 100644 index 0000000..6b94572 --- /dev/null +++ b/src/popup/MothershipManager.js @@ -0,0 +1,170 @@ +/** + * MothershipManager โ€” Popup UI for connecting the extension to the Mothership backend. + * Handles URL/token input, workspace selection, connect/disconnect, and status display. + */ +export class MothershipManager { + constructor() { + this.urlInput = document.getElementById('mothershipUrl'); + this.tokenInput = document.getElementById('mothershipToken'); + this.workspaceSelect = document.getElementById('mothershipWorkspaceSelect'); + this.connectBtn = document.getElementById('mothershipConnectBtn'); + this.disconnectBtn = document.getElementById('mothershipDisconnectBtn'); + this.statusDot = document.getElementById('mothershipStatusDot'); + this.statusText = document.getElementById('mothershipStatusText'); + this._workspaces = []; + + this._bindEvents(); + this._loadState(); + } + + _bindEvents() { + this.connectBtn.addEventListener('click', () => this._connect()); + this.disconnectBtn.addEventListener('click', () => this._disconnect()); + // Auto-fetch workspaces when token field loses focus + this.tokenInput.addEventListener('blur', () => this._fetchWorkspaces()); + this.workspaceSelect.addEventListener('change', () => { + const wsId = this.workspaceSelect.value; + if (wsId) chrome.storage.local.set({ mothershipWorkspaceId: wsId }); + }); + } + + async _loadState() { + const data = await new Promise(r => + chrome.storage.local.get(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId'], r) + ); + + if (data.mothershipUrl) this.urlInput.value = data.mothershipUrl; + if (data.mothershipToken) this.tokenInput.value = data.mothershipToken; + + // Check connection status + chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => { + this._updateUI(res?.connected, data.mothershipWorkspaceId); + }); + + // If we have URL + token, fetch workspaces to populate selector + if (data.mothershipUrl && data.mothershipToken) { + this._fetchWorkspaces(data.mothershipWorkspaceId); + } + } + + async _fetchWorkspaces(selectedId) { + const url = this.urlInput.value.trim().replace(/\/+$/, ''); + const token = this.tokenInput.value.trim(); + if (!url || !token) return; + + try { + const res = await fetch(`${url}/api/v1/workspaces`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return; + this._workspaces = await res.json(); + + // Populate selector + this.workspaceSelect.innerHTML = ''; + for (const ws of this._workspaces) { + const opt = document.createElement('option'); + opt.value = ws.id; + opt.textContent = ws.name; + if (ws.id === selectedId) opt.selected = true; + this.workspaceSelect.appendChild(opt); + } + this.workspaceSelect.style.display = this._workspaces.length > 1 ? '' : 'none'; + + // Auto-select if only one + if (this._workspaces.length === 1 && !selectedId) { + this.workspaceSelect.value = this._workspaces[0].id; + } + } catch {} + } + + async _connect() { + const url = this.urlInput.value.trim().replace(/\/+$/, ''); + const token = this.tokenInput.value.trim(); + if (!url || !token) return; + + this.connectBtn.textContent = 'Connecting...'; + this.connectBtn.disabled = true; + + try { + const healthRes = await fetch(`${url}/api/v1/health`); + if (!healthRes.ok) throw new Error('Server unreachable'); + + // Fetch workspaces + const wsRes = await fetch(`${url}/api/v1/workspaces`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!wsRes.ok) { + this._showError('Invalid token'); + this.connectBtn.textContent = 'Connect'; + this.connectBtn.disabled = false; + return; + } + this._workspaces = await wsRes.json(); + if (!this._workspaces.length) { + this._showError('No workspace found'); + this.connectBtn.textContent = 'Connect'; + this.connectBtn.disabled = false; + return; + } + + // Use selected workspace or default to first + const workspaceId = this.workspaceSelect.value || this._workspaces[0].id; + + // Populate selector + this.workspaceSelect.innerHTML = ''; + for (const ws of this._workspaces) { + const opt = document.createElement('option'); + opt.value = ws.id; + opt.textContent = ws.name; + if (ws.id === workspaceId) opt.selected = true; + this.workspaceSelect.appendChild(opt); + } + this.workspaceSelect.style.display = this._workspaces.length > 1 ? '' : 'none'; + + chrome.storage.local.set({ mothershipUrl: url, mothershipToken: token, mothershipWorkspaceId: workspaceId }); + } catch { + this._showError('Cannot reach server'); + this.connectBtn.textContent = 'Connect'; + this.connectBtn.disabled = false; + return; + } + + // Tell background to connect + chrome.runtime.sendMessage({ action: 'mothershipConnect', url, token }, () => { + setTimeout(() => { + chrome.runtime.sendMessage({ action: 'mothershipStatus' }, (res) => { + this._updateUI(res?.connected); + this.connectBtn.textContent = 'Connect'; + this.connectBtn.disabled = false; + }); + }, 1000); + }); + } + + _disconnect() { + chrome.runtime.sendMessage({ action: 'mothershipDisconnect' }); + chrome.storage.local.remove(['mothershipUrl', 'mothershipToken', 'mothershipWorkspaceId']); + this.urlInput.value = ''; + this.tokenInput.value = ''; + this.workspaceSelect.innerHTML = ''; + this.workspaceSelect.style.display = 'none'; + this._updateUI(false); + } + + _updateUI(connected, _selectedWsId) { + this.statusDot.style.background = connected ? '#34c759' : '#aaa'; + this.statusDot.style.boxShadow = connected ? '0 0 6px rgba(52,199,89,0.4)' : 'none'; + this.statusText.textContent = connected ? 'Connected' : 'Disconnected'; + this.connectBtn.style.display = connected ? 'none' : ''; + this.disconnectBtn.style.display = connected ? '' : 'none'; + } + + _showError(msg) { + this.statusText.textContent = msg; + this.statusText.style.color = '#ff3b30'; + setTimeout(() => { + this.statusText.style.color = ''; + this.statusText.textContent = 'Disconnected'; + }, 3000); + } +} diff --git a/src/popup/popup.html b/src/popup/popup.html index 4e67d23..ef552a2 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -1705,6 +1705,34 @@
+ +
+
+
+ + + + + Mothership +
+
+
+
+ + Disconnected +
+ + + +
+ + +
+
+
+