From 6a58ec4d0c9dae1a4a3e6bc43a597a6464fca9b6 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Tue, 18 Aug 2026 15:19:19 +0700 Subject: [PATCH] chore: move NexusAI out to its own private repository (Hidosi/nexusai) --- NexusAI/.env.example | 49 - NexusAI/.gitignore | 25 - NexusAI/.opencode/opencode.json | 18 - NexusAI/AGENTS.md | 238 -- NexusAI/README.md | 184 -- NexusAI/TEST_RESUME.md | 372 --- NexusAI/package-lock.json | 2391 ----------------- NexusAI/package.json | 21 - NexusAI/packages/admin/package.json | 24 - NexusAI/packages/admin/public/app.js | 328 --- NexusAI/packages/admin/public/index.html | 224 -- NexusAI/packages/admin/src/context.ts | 29 - NexusAI/packages/admin/src/routes.ts | 193 -- NexusAI/packages/admin/src/server.ts | 68 - NexusAI/packages/admin/tsconfig.json | 17 - NexusAI/packages/core/package.json | 22 - NexusAI/packages/core/src/config.ts | 64 - NexusAI/packages/core/src/db/journal.ts | 158 -- .../packages/core/src/db/providers-store.ts | 192 -- NexusAI/packages/core/src/index.ts | 16 - NexusAI/packages/core/src/media/content.ts | 36 - NexusAI/packages/core/src/media/storage.ts | 120 - .../core/src/providers/local-stt/whisper.ts | 139 - .../core/src/providers/model-catalog.ts | 205 -- .../core/src/providers/nordrouter/client.ts | 26 - .../core/src/providers/nordrouter/index.ts | 26 - .../core/src/providers/nordrouter/media.ts | 193 -- .../core/src/providers/nordrouter/search.ts | 72 - .../packages/core/src/providers/registry.ts | 72 - NexusAI/packages/core/src/providers/types.ts | 106 - NexusAI/packages/core/src/security/crypto.ts | 88 - .../packages/core/src/security/resolver.ts | 107 - NexusAI/packages/core/src/shared/errors.ts | 53 - NexusAI/packages/core/src/shared/http.ts | 153 -- NexusAI/packages/core/src/shared/logger.ts | 42 - NexusAI/packages/core/src/shared/polling.ts | 41 - NexusAI/packages/core/tsconfig.json | 18 - NexusAI/packages/mcp-server/package.json | 25 - NexusAI/packages/mcp-server/src/context.ts | 96 - NexusAI/packages/mcp-server/src/handlers.ts | 433 --- NexusAI/packages/mcp-server/src/index.ts | 56 - NexusAI/packages/mcp-server/src/schemas.ts | 151 -- NexusAI/packages/mcp-server/src/tools.ts | 84 - NexusAI/packages/mcp-server/tsconfig.json | 17 - NexusAI/sidecar/stt_server.py | 131 - 45 files changed, 7123 deletions(-) delete mode 100644 NexusAI/.env.example delete mode 100644 NexusAI/.gitignore delete mode 100644 NexusAI/.opencode/opencode.json delete mode 100644 NexusAI/AGENTS.md delete mode 100644 NexusAI/README.md delete mode 100644 NexusAI/TEST_RESUME.md delete mode 100644 NexusAI/package-lock.json delete mode 100644 NexusAI/package.json delete mode 100644 NexusAI/packages/admin/package.json delete mode 100644 NexusAI/packages/admin/public/app.js delete mode 100644 NexusAI/packages/admin/public/index.html delete mode 100644 NexusAI/packages/admin/src/context.ts delete mode 100644 NexusAI/packages/admin/src/routes.ts delete mode 100644 NexusAI/packages/admin/src/server.ts delete mode 100644 NexusAI/packages/admin/tsconfig.json delete mode 100644 NexusAI/packages/core/package.json delete mode 100644 NexusAI/packages/core/src/config.ts delete mode 100644 NexusAI/packages/core/src/db/journal.ts delete mode 100644 NexusAI/packages/core/src/db/providers-store.ts delete mode 100644 NexusAI/packages/core/src/index.ts delete mode 100644 NexusAI/packages/core/src/media/content.ts delete mode 100644 NexusAI/packages/core/src/media/storage.ts delete mode 100644 NexusAI/packages/core/src/providers/local-stt/whisper.ts delete mode 100644 NexusAI/packages/core/src/providers/model-catalog.ts delete mode 100644 NexusAI/packages/core/src/providers/nordrouter/client.ts delete mode 100644 NexusAI/packages/core/src/providers/nordrouter/index.ts delete mode 100644 NexusAI/packages/core/src/providers/nordrouter/media.ts delete mode 100644 NexusAI/packages/core/src/providers/nordrouter/search.ts delete mode 100644 NexusAI/packages/core/src/providers/registry.ts delete mode 100644 NexusAI/packages/core/src/providers/types.ts delete mode 100644 NexusAI/packages/core/src/security/crypto.ts delete mode 100644 NexusAI/packages/core/src/security/resolver.ts delete mode 100644 NexusAI/packages/core/src/shared/errors.ts delete mode 100644 NexusAI/packages/core/src/shared/http.ts delete mode 100644 NexusAI/packages/core/src/shared/logger.ts delete mode 100644 NexusAI/packages/core/src/shared/polling.ts delete mode 100644 NexusAI/packages/core/tsconfig.json delete mode 100644 NexusAI/packages/mcp-server/package.json delete mode 100644 NexusAI/packages/mcp-server/src/context.ts delete mode 100644 NexusAI/packages/mcp-server/src/handlers.ts delete mode 100644 NexusAI/packages/mcp-server/src/index.ts delete mode 100644 NexusAI/packages/mcp-server/src/schemas.ts delete mode 100644 NexusAI/packages/mcp-server/src/tools.ts delete mode 100644 NexusAI/packages/mcp-server/tsconfig.json delete mode 100644 NexusAI/sidecar/stt_server.py diff --git a/NexusAI/.env.example b/NexusAI/.env.example deleted file mode 100644 index 1c0e0ad..0000000 --- a/NexusAI/.env.example +++ /dev/null @@ -1,49 +0,0 @@ -# --- NordRouter --- -# API key from your NordRouter account (starts with sk-) -NORDROUTER_API_KEY=sk-your-key-here -NORDROUTER_BASE_URL=https://nordrouter.com - -# --- NexusAI general --- -NEXUS_DEFAULT_PROVIDER=nordrouter -# Where generated media files and the SQLite journal are stored -NEXUS_MEDIA_DIR=./media-output -# Optional: custom SQLite path (defaults to /nexus.sqlite) -# NEXUS_DB_PATH=./media-output/nexus.sqlite -# Max bytes for embedding image/audio directly in MCP results (default 10MB) -NEXUS_EMBED_MAX_BYTES=10485760 -# Log level: debug | info | warn | error (logs go to stderr) -NEXUS_LOG_LEVEL=info - -# --- Managed provider keys (optional, for admin-managed keys) --- -# Master secret used to encrypt provider API keys stored in SQLite (AES-256-GCM). -# Accepts a 64-char hex string or any passphrase. NEVER stored in the DB. -# If unset, only env keys (e.g. NORDROUTER_API_KEY) are used. -# Generate: openssl rand -hex 32 -# NEXUS_MASTER_KEY= -# Optional separate DB for the providers store (defaults to NEXUS_DB_PATH) -# NEXUS_PROVIDERS_DB_PATH=./media-output/nexus.sqlite -# Key precedence: _API_KEY env > encrypted DB entry - -# --- Local STT (faster-whisper via uv sidecar) --- -# Launcher: "uv" (recommended, auto-installs deps) or a python interpreter path -NEXUS_STT_PYTHON=uv -# Whisper size (tiny|base|small|medium|large-v3) OR an absolute path to a -# pre-downloaded CTranslate2 model directory -NEXUS_STT_MODEL=small -NEXUS_STT_DEVICE=auto # auto | cpu | cuda -NEXUS_STT_COMPUTE=auto # auto | int8 | float16 -# Offline mode: require a locally present model (no downloads) -# NEXUS_STT_LOCAL_ONLY=1 -# Cache dir for downloaded models -# NEXUS_STT_DOWNLOAD_ROOT=./models -# Optional custom sidecar path -# NEXUS_STT_SIDECAR=/abs/path/to/sidecar/stt_server.py -# HuggingFace mirror (for restricted networks) -# HF_ENDPOINT=https://hf-mirror.com - -# --- Admin web UI (packages/admin) --- -NEXUS_ADMIN_HOST=127.0.0.1 -NEXUS_ADMIN_PORT=4123 - -# --- Proxy (optional) --- -# NEXUS_HTTP_PROXY=http://proxy:8080 diff --git a/NexusAI/.gitignore b/NexusAI/.gitignore deleted file mode 100644 index 118649b..0000000 --- a/NexusAI/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ - -# secrets -api-sec.txt -.env -.env.* -!.env.example - -# node -node_modules/ -**/dist/ -*.tsbuildinfo - -# data / media -media-output/ -*.sqlite -*.sqlite-journal -*.db - -# python -sidecar/.venv/ -__pycache__/ -*.pyc - -# os -.DS_Store diff --git a/NexusAI/.opencode/opencode.json b/NexusAI/.opencode/opencode.json deleted file mode 100644 index ab30cc4..0000000 --- a/NexusAI/.opencode/opencode.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "nexusai": { - "type": "local", - "command": [ - "node", - "/home/user/moder/mcp/NexusAI/packages/mcp-server/dist/index.js" - ], - "env": { - "NORDROUTER_API_KEY": "sk-your-key-here", - "NEXUS_MEDIA_DIR": "/home/user/moder/mcp/NexusAI/media-output", - "NEXUS_STT_MODEL": "small" - }, - "enabled": true - } - } -} diff --git a/NexusAI/AGENTS.md b/NexusAI/AGENTS.md deleted file mode 100644 index e37b39d..0000000 --- a/NexusAI/AGENTS.md +++ /dev/null @@ -1,238 +0,0 @@ -# AGENTS.md — NexusAI - -> Карта проекта для ИИ-агентов и разработчиков. Читай этот файл первым, чтобы не перечитывать весь код. -> Обновляй раздел **«Последние изменения»** и карту при значимых правках. - ---- - -## 1. Что это - -**NexusAI** — универсальный **MCP-сервис** для агентов. Даёт один набор инструментов для: -- **TTS** (text-to-speech), **STT** (speech-to-text, локально), -- генерации **изображений / видео / музыки**, -- **веб-поиска** (search-augmented модели), -- **vision** (описание изображения chat-моделью), -- служебных операций: список моделей, оценка стоимости, загрузка файла, статистика. - -Плюс **веб-админка** (Fastify) для галереи контента, учёта трат и управления провайдерами/ключами. - -Первый и основной провайдер — **NordRouter** (`https://nordrouter.com`, OpenAI-совместимый). -Архитектура провайдер-агностична: адаптеры добавляются в `registry`, слой инструментов не меняется. - ---- - -## 2. Технологии - -- **TypeScript**, monorepo через **npm workspaces** (CommonJS, `tsc`). -- **MCP**: `@modelcontextprotocol/sdk` (stdio-транспорт). -- **HTTP**: `undici` (fetch + ProxyAgent + FormData). -- **БД**: `better-sqlite3` (журнал + провайдеры). -- **Админка**: `fastify` + `@fastify/static`, фронт — ванильный HTML/CSS/JS (без сборки). -- **Схемы**: `zod` (+ `zod-to-json-schema` для MCP inputSchema). -- **STT**: Python sidecar `faster-whisper`, запускается через `uv`. - ---- - -## 3. Карта проекта - -``` -NexusAI/ -├── AGENTS.md # ← этот файл -├── README.md # пользовательская документация -├── TEST_RESUME.md # лог ручных тестов (search/STT/TTS/image/video/vision) -├── .env.example # все переменные окружения с описанием -├── package.json # workspaces + скрипты build/start/admin -├── sidecar/ -│ └── stt_server.py # faster-whisper sidecar (JSON in argv → JSON stdout) -└── packages/ - ├── core/ # @nexusai/core — общая логика, БД, провайдеры - ├── mcp-server/ # @nexusai/mcp-server — MCP stdio-сервер (инструменты) - └── admin/ # @nexusai/admin — веб-дашборд -``` - -### 3.1 `packages/core` (`@nexusai/core`) - -| Файл | Назначение | -|---|---| -| `src/config.ts` | `loadConfig()` — читает все `NEXUS_*` / `NORDROUTER_*` env → `NexusConfig`. | -| `src/index.ts` | Реэкспорт всего публичного API core. | -| `src/db/journal.ts` | `Journal` — SQLite журнал генераций: `record/getById/list/deleteById/stats`. | -| `src/db/providers-store.ts` | `ProvidersStore` — провайдеры + **зашифрованные** ключи (AES-256-GCM). Отдаёт `ProviderView` с маской. | -| `src/security/crypto.ts` | `KeyCipher` (шифрование ключей), `maskSecret`. `enabled` = задан ли мастер-ключ. | -| `src/security/resolver.ts` | `KeyResolver` — эффективные настройки провайдера. **Приоритет ключа: ENV > DB**. Грациозный фолбэк, если DB-ключ нечитаем. | -| `src/media/storage.ts` | `MediaStorage` — скачивание URL/base64 на диск, `mimeToExt/extToMime`, `cleanup(ttlDays)`. | -| `src/media/content.ts` | `buildEmbeddedContent()` — упаковка файла в MCP-контент (image/audio) с лимитом. | -| `src/providers/types.ts` | Базовые типы: `Capability`, `MediaProvider`, `SearchProvider`, `SttProvider`, `ModelInfo` и пр. | -| `src/providers/registry.ts` | `ProviderRegistry` — карта name+capability → адаптер. | -| `src/providers/model-catalog.ts` | **`ModelCatalog`** — фоновая синхронизация каталога моделей + **fuzzy-подбор** модели (LCS + токены, дешёвая при равенстве), `ModelResolutionError` со списком моделей. | -| `src/providers/nordrouter/client.ts` | `NordRouterClient` — низкоуровневый HTTP (Bearer, rate-limit). | -| `src/providers/nordrouter/media.ts` | `NordRouterMediaProvider` — `/media/generate` + polling, `estimate`, `upload` (data-URL, MIME по расширению), `listModels`, `checkKey`. Capabilities: `tts,image,video,music`. | -| `src/providers/nordrouter/search.ts` | `NordRouterSearchProvider` — веб-поиск через `/v1/chat/completions` (perplexity/sonar-pro), citations из annotations. | -| `src/providers/nordrouter/index.ts` | `createNordRouterProviders()` — фабрика media+search. | -| `src/providers/local-stt/whisper.ts` | `LocalWhisperProvider` — спавнит sidecar, materialize аудио (path/url/base64). | -| `src/shared/http.ts` | `HttpClient` (retry, rate-limit, proxy, **FormData**), `RateLimiter`. | -| `src/shared/errors.ts` | `NexusError` + `codeFromHttpStatus`. | -| `src/shared/logger.ts` | `createLogger` — **пишет только в stderr** (stdout занят MCP). | -| `src/shared/polling.ts` | `pollUntilDone`, `sleep`. | - -### 3.2 `packages/mcp-server` (`@nexusai/mcp-server`) - -| Файл | Назначение | -|---|---| -| `src/index.ts` | Точка входа: поднимает MCP `Server` на stdio, регистрирует `ListTools`/`CallTool`. | -| `src/context.ts` | `buildContext()` — собирает `AppContext`: registry, storage, journal, providersStore, **catalogs** (Map с авто-синком), defaultProvider. | -| `src/schemas.ts` | Zod-схемы входа для всех инструментов. | -| `src/tools.ts` | `TOOLS[]` — описания + JSON Schema (генерится из zod). | -| `src/handlers.ts` | `Handlers` — реализация всех инструментов, запись в журнал, **резолв модели через catalog**, запись STT-транскрипта в `.md`. | - -### 3.3 `packages/admin` (`@nexusai/admin`) - -| Файл | Назначение | -|---|---| -| `src/server.ts` | Fastify: раздаёт `/media/:file` (с корректным Content-Type, incl. `text/markdown`), статику `public/`, API. | -| `src/context.ts` | `buildAdminContext()` — journal + providersStore + cipher (без registry). | -| `src/routes.ts` | REST API (см. §6). `KNOWN_TYPES = ['nordrouter','openai']`. | -| `public/index.html` | UI: вкладки, темы, галерея, модалка, автообновление. Ванильный CSS через `--vars`. | -| `public/app.js` | Логика UI: fetch API, рендер плиток, модалка, аудиоплеер, авто-refresh. | - ---- - -## 4. MCP-инструменты (11) - -| Инструмент | Что делает | Ключевые аргументы | -|---|---|---| -| `nexus_tts` | Синтез речи | `model` (default `audio/elevenlabs-tts`), `input.text/voice/...` | -| `nexus_image` | Генерация/редактирование картинок | `model` (default `image/nano-banana-2`), `input.prompt/image/...` | -| `nexus_video` | t2v / i2v (может занять минуты) | `model` (default `video/kling-2.6-t2v`), `input.prompt/image/...` | -| `nexus_music` | Генерация музыки | `model` (default `music/suno-v5`), `input...` | -| `nexus_stt` | Распознавание речи (локально) | `input.audioPath/audioUrl/audioBase64/language/model` | -| `nexus_web_search` | Веб-поиск с источниками | `query`, `recency`, `maxResults`, `model` | -| `nexus_vision` | Описание изображения chat-моделью | `image` (url/path/base64), `prompt`, `model` (default `google/gemini-3.1-flash-lite`) | -| `nexus_media_models` | Список моделей провайдера | `type`, `search` | -| `nexus_media_estimate` | Оценка стоимости до генерации | `model`, `input` | -| `nexus_media_upload` | Загрузка файла к провайдеру → URL | `path`, `name`, `mimeType` | -| `nexus_usage_stats` | Статистика из журнала | `sinceDays`, `list`, `limit` | - -**Медиа-пайплайн** (`Handlers.media`): резолв модели через `ModelCatalog` → `provider.generate` (polling) → скачивание файла в media-dir → запись в журнал → возврат JSON (+ `note` при подмене модели). - -**Fuzzy-резолв модели**: кривое имя → ближайшая (при равенстве — **дешевле**); нет совпадений → `BAD_REQUEST` с `available` (список категории, дешёвые первыми) и подсказкой попросить пользователя уточнить. - ---- - -## 5. Возможности (capabilities) - -`Capability = 'tts' | 'stt' | 'image' | 'video' | 'music' | 'search'` -(в журнале также встречается `vision`). - -Маппинг NordRouter `type` → capability (в `model-catalog.ts`): `audio→tts`, `image→image`, `video→video`, `music→music`. - ---- - -## 6. Admin REST API - -| Метод | Путь | Назначение | -|---|---|---| -| GET | `/api/providers` | Список провайдеров (маска ключа), `knownTypes`, `cryptoEnabled`. | -| POST | `/api/providers` | Upsert провайдера (ключ шифруется). | -| POST | `/api/providers/:name/enabled` | Вкл/выкл. | -| POST | `/api/providers/:name/default` | Сделать default. | -| DELETE | `/api/providers/:name` | Удалить. | -| POST | `/api/providers/:name/check` | Проверка ключа реальным вызовом. | -| GET | `/api/stats?sinceDays=` | Статистика трат. | -| GET | `/api/generations?limit=&offset=&capability=` | Галерея (добавляет `mediaUrl`). | -| GET | `/api/generations/:id` | Одна запись. | -| DELETE | `/api/generations/:id` | Удалить запись + файл (только внутри media-dir). | -| GET | `/media/:file` | Раздача файла (Content-Type по расширению). | - ---- - -## 7. Переменные окружения (главное) - -```bash -# NordRouter -NORDROUTER_API_KEY=sk-... -NORDROUTER_BASE_URL=https://nordrouter.com -# Общие -NEXUS_DEFAULT_PROVIDER=nordrouter -NEXUS_MEDIA_DIR=./media-output # медиа + SQLite -NEXUS_DB_PATH=/nexus.sqlite # опц. -NEXUS_MASTER_KEY= # шифрование ключей в БД (иначе только ENV-ключи) -NEXUS_EMBED_MAX_BYTES=10485760 -NEXUS_LOG_LEVEL=info # логи → stderr -# STT -NEXUS_STT_PYTHON=uv -NEXUS_STT_MODEL=small # tiny|base|small|medium|large-v3 ИЛИ путь к CT2-модели -NEXUS_STT_DEVICE=auto ; NEXUS_STT_COMPUTE=auto -NEXUS_STT_LOCAL_ONLY=1 # оффлайн (без скачивания) -HF_ENDPOINT=https://hf-mirror.com # зеркало HF в закрытых сетях -# Admin -NEXUS_ADMIN_HOST=127.0.0.1 ; NEXUS_ADMIN_PORT=4123 -# Proxy (опц.) -NEXUS_HTTP_PROXY=http://proxy:8080 -``` - -Приоритет ключа: `_API_KEY` (env) **>** зашифрованная запись в БД. - ---- - -## 8. Команды - -```bash -npm install -npm run build # core → mcp-server → admin (по отдельности: build:core|build:mcp|build:admin) -npm run start # запустить MCP-сервер (stdio) -npm run admin # запустить админку (http://127.0.0.1:4123) -``` - -MCP работает по stdio → **никогда не писать в stdout** кроме протокола; все логи идут в stderr. - -### Локальный запуск админки в этой среде (фоновые процессы реапятся) - -```bash -tmux new-session -d -s nexusadmin -c /home/user/moder/mcp/NexusAI \ - "export NEXUS_MEDIA_DIR=/tmp/opencode/nexus-media NEXUS_MASTER_KEY=... NEXUS_ADMIN_PORT=4123; \ - exec node packages/admin/dist/server.js >/tmp/opencode/admin_live.log 2>&1" -``` - -### Тестирование инструментов без полного MCP-хендшейка - -Импортировать `buildContext` + `Handlers` из `dist/` и звать `handlers.handle('nexus_*', args)`. -⚠️ Скрипт должен лежать **внутри** `packages/mcp-server/` (иначе не резолвятся node_modules workspace). -Env для теста: `NEXUS_MEDIA_DIR`, `NEXUS_MASTER_KEY` (тот же, что при сохранении ключа), STT-переменные. - ---- - -## 9. Важные особенности / подводные камни - -- **stdout только для MCP.** Логи — в stderr (`createLogger`). -- **Ключ мастер-шифрования обязателен** для чтения ключей из БД; иначе только ENV-ключи. Без ключа сервис не падает (грациозный фолбэк). -- **NordRouter upload** ждёт `data:;base64,...` в поле `data`. MIME определяется по расширению — иначе `HTTP 415`. -- **Kling i2v** не принимает прямую ссылку на сгенерированный контент — нужен URL из `nexus_media_upload` (`?raw=1`). -- **Видео-модели** часто требуют полный набор полей (`duration`, `aspect_ratio`, `resolution`, ...), иначе `HTTP 400`. -- **STT** не качает модель в закрытой сети (HF недоступен). Варианты: `HF_ENDPOINT`, локальная модель + `NEXUS_STT_LOCAL_ONLY=1` (модель `faster-whisper-*` можно взять с ModelScope). -- **STT-транскрипт** сохраняется как `.md` в media-dir и виден в галерее/модалке (не путать с исходным аудио). -- **Стоимость видео** может превышать `estUsd` (например, Kling 2.6 i2v: est `$0.34` → факт `$0.69`). -- Некоторые модели бывают временно недоступны («Генерация временно недоступна — средства возвращены»). -- **ID моделей NordRouter для chat/vision**: `google/gemini-3.1-flash-lite`, `openai/gpt-5.x` (НЕ `gpt-4o`). - ---- - -## 10. Последние изменения - -Свежие сверху. Держи актуальным. - -- **`8bd2e89`** — STT пишет транскрипт в `.md` (виден в галерее/модалке); `ModelCatalog` (фоновый синк + fuzzy-резолв с дешёвым фолбэком и подсказками); UI: аудиоплеер (кнопка в карточке + плеер в модалке), просмотр текстовых `.md`, автообновление активной вкладки без перезагрузки; `/media` отдаёт корректный Content-Type. -- **`b2f57cc`** — новый инструмент `nexus_vision`; фикс `nexus_media_upload` (MIME по расширению, чинит 415); `HttpClient` поддерживает FormData. -- **`c65f910`** — STT больше не пишет путь исходного аудио как `filePath` (убрана битая ссылка в галерее). -- **`1bc3746`** — Admin UX: светлая/тёмная тема, пагинация галереи, модалка + удаление, тип провайдера `openai`. -- **`6ff2b27`** — фикс краша MCP при наличии ключа в БД без `NEXUS_MASTER_KEY`. -- **`679e564`** — Admin (Phase 2): дашборд трат, галерея, управление провайдерами. - ---- - -## 11. Куда добавлять новое - -- **Новый провайдер** → адаптер в `core/src/providers//`, регистрация в `mcp-server/src/context.ts`, тип в `admin` `KNOWN_TYPES` при необходимости. -- **Новый инструмент** → схема в `schemas.ts`, ветка в `Handlers.handle`, описание в `tools.ts`. -- **Новое поле журнала** → миграция в `Journal.init`, обновить `GenerationRecord` и SELECT-колонки в `getById/list`. -- **UI** → `public/index.html` (разметка/CSS) + `public/app.js` (логика). Сборки нет — правки применяются сразу. diff --git a/NexusAI/README.md b/NexusAI/README.md deleted file mode 100644 index 5908f50..0000000 --- a/NexusAI/README.md +++ /dev/null @@ -1,184 +0,0 @@ -# NexusAI — универсальный MCP-сервис для агентов - -Единый MCP-сервер (TypeScript, stdio), дающий агентам провайдер-абстрагированный доступ к: - -- **TTS** — озвучка текста -- **STT** — распознавание речи (локально, faster-whisper) -- **Изображения** — генерация и правка -- **Видео** — text→video / image→video -- **Музыка** — генерация треков -- **Веб-поиск** — ответ с цитатами (search-augmented модели) - -Первый провайдер — **NordRouter**. Архитектура расширяемая: RouterAI, GPTunnel, OpenRouter и облачные STT добавляются как новые адаптеры без изменения MCP-инструментов. Каждая операция логируется в SQLite (провайдер, модель, стоимость, статус, файл) — основа для будущей веб-админки. - -## Структура - -``` -NexusAI/ -├── packages/ -│ ├── core/ # провайдеры, media-storage, journal, ключи, типы, registry -│ ├── mcp-server/ # stdio MCP-сервер (10 инструментов) -│ └── admin/ # веб-админка (Fastify + лёгкий фронт) -├── sidecar/ # faster-whisper STT (PEP 723, запуск через uv) -├── .env.example -└── .opencode/opencode.json -``` - -## Установка - -```bash -cd NexusAI -npm install -npm run build -``` - -> `better-sqlite3` собирается нативно. Если сборка прервалась: `npm rebuild better-sqlite3`. - -Для STT нужен [`uv`](https://docs.astral.sh/uv/) и `ffmpeg`. Зависимости whisper `uv` ставит автоматически при первом запуске. - -## Подключение к opencode - -`.opencode/opencode.json`: - -```json -{ - "mcp": { - "nexusai": { - "type": "local", - "command": ["node", "/абс/путь/NexusAI/packages/mcp-server/dist/index.js"], - "env": { - "NORDROUTER_API_KEY": "sk-...", - "NEXUS_MEDIA_DIR": "/абс/путь/NexusAI/media-output", - "NEXUS_STT_MODEL": "small" - } - } - } -} -``` - -Переменные окружения — см. `.env.example`. - -## Инструменты - -| Инструмент | Назначение | -|---|---| -| `nexus_tts` | текст → речь (`audio/elevenlabs-tts`, `audio/gemini-3.1-flash-tts`, …) | -| `nexus_image` | генерация/правка изображений (`image/*`) | -| `nexus_video` | видео из текста/фото (`video/*`) | -| `nexus_music` | музыка (`music/*`) | -| `nexus_stt` | речь → текст (локально) | -| `nexus_web_search` | веб-поиск с цитатами (`perplexity/sonar*`, `search/*`) | -| `nexus_media_models` | список моделей провайдера (id, тип, цена) | -| `nexus_media_estimate` | оценка стоимости до запуска | -| `nexus_media_upload` | загрузка исходника (фото/аудио/видео) | -| `nexus_usage_stats` | траты и история из журнала | - -### Единый формат вызова - -```json -{ - "provider": "nordrouter", - "model": "audio/elevenlabs-tts", - "input": { "text": "Привет, мир", "voice": "James · Husky, Engaging and Bold" }, - "output": { "save": true, "embed": false } -} -``` - -- `provider` опционален (default из `NEXUS_DEFAULT_PROVIDER`). -- `input` пропускает любые модель-специфичные поля (`aspect_ratio`, `resolution`, `duration`, `sound`, …). -- `output.save` — сохранять файл локально (по умолчанию да). -- `output.embed` — встроить image/audio прямо в ответ MCP (с лимитом размера). - -### Единый формат ответа - -```json -{ - "provider": "nordrouter", - "capability": "tts", - "model": "audio/elevenlabs-tts", - "status": "done", - "file_path": "/.../media-output/audio-...wav", - "mime_type": "audio/wav", - "result_url": "https://nordrouter.com/media/file/...", - "result_url_2": null, - "cost_usd": 0.0049 -} -``` - -`result_url_2` заполняется для музыкальных моделей, отдающих два трека (Suno). - -## Как это работает - -- **Media** асинхронно: `POST /media/generate` → polling `GET /media/job/:id` → скачивание `result_url` в `NEXUS_MEDIA_DIR`. Таймаут зависит от типа (картинки — быстро, видео/музыка — до минут). -- **Web search** через OpenAI-совместимый `/v1/chat/completions` с sonar/`search/*`; цитаты берутся из `message.annotations[].url_citation`. -- **STT** локально: TS запускает `uv run sidecar/stt_server.py`; faster-whisper декодирует аудио (mp3/wav/ogg/m4a/webm) и возвращает `{text, language, segments}`. -- **Журнал** SQLite (`nexus.sqlite` в media-dir) пишет каждую операцию; `nexus_usage_stats` агрегирует траты. -- MCP работает по stdio — **все логи идут в stderr**. - -## Ключи провайдеров - -Ключи можно задавать двумя способами, с приоритетом **env → БД**: - -1. **Env (приоритет)** — `NORDROUTER_API_KEY` (и `_API_KEY` для других провайдеров). Просто для CI и локального запуска. -2. **Зашифрованная БД** — управляется из будущей админки. Ключи хранятся в SQLite, зашифрованные **AES-256-GCM** на мастер-ключе `NEXUS_MASTER_KEY` (сам мастер-ключ — только в env, в БД его нет). - -Свойства слоя: -- В git/логи/БД сырой ключ не попадает; в UI показывается только маска `sk-…last4`. -- Бэкап БД не раскрывает ключи (нужен мастер-ключ). -- Ротация `NEXUS_MASTER_KEY` инвалидирует зашифрованные ключи — их нужно ввести заново. -- Без `NEXUS_MASTER_KEY` работают только env-ключи (сервер предупреждает в stderr). - -Программное управление (пока до админки) — через `@nexusai/core`: - -```ts -import { KeyCipher, ProvidersStore } from '@nexusai/core'; -const cipher = new KeyCipher(process.env.NEXUS_MASTER_KEY); -const store = new ProvidersStore('./media-output/nexus.sqlite', cipher); -store.upsert({ name: 'nordrouter', type: 'nordrouter', apiKey: 'sk-...', isDefault: true }); -store.setEnabled('nordrouter', true); -store.listViews(); // masked view for UI -``` - -Генерация мастер-ключа: `openssl rand -hex 32`. - -## STT в ограниченных сетях - -faster-whisper скачивает модель с HuggingFace. Если `huggingface.co` недоступен: - -- задать зеркало: `HF_ENDPOINT=https://hf-mirror.com` -- либо предзагрузить модель и указать путь: `NEXUS_STT_MODEL=/abs/model-dir` + `NEXUS_STT_LOCAL_ONLY=1` -- либо кэш-директорию: `NEXUS_STT_DOWNLOAD_ROOT=./models` - -## Веб-админка - -Отдельный пакет `@nexusai/admin` (Fastify + одностраничный фронт) на общем `core`. Читает тот же SQLite и media-каталог. - -```bash -npm run build -NEXUS_MASTER_KEY=$(openssl rand -hex 32) \ -NEXUS_MEDIA_DIR=./media-output \ -npm run admin -# → http://127.0.0.1:4123 -``` - -Возможности: -- **Тема** — переключатель светлая/тёмная (сохраняется в localStorage). -- **Потребление** — дашборд трат: суммарно, по провайдерам, по возможностям, по дням (из журнала). -- **Галерея** — просмотр сгенерированного контента (изображения/аудио/видео), фильтр по возможности, пагинация 50/100/150/200, модальное окно на весь экран, удаление контента (файл + запись). -- **Провайдеры** — добавить/включить/выключить, назначить default, ввести ключ (шифруется), **проверить ключ** реальным вызовом, типы `nordrouter` и `openai`. Ключи показываются только маской. - -Переменные: `NEXUS_ADMIN_HOST` (default `127.0.0.1`), `NEXUS_ADMIN_PORT` (default `4123`), `NEXUS_MASTER_KEY` (для ключей). - -> Важно: изменения провайдеров/ключей в админке подхватываются MCP-сервером **при рестарте** (hot-reload — в планах). По умолчанию админка слушает только localhost. - -## Расширение: новый провайдер - -1. Реализовать `MediaProvider` / `SearchProvider` / `SttProvider` из `@nexusai/core`. -2. Зарегистрировать в `packages/mcp-server/src/context.ts`. - -Инструменты и схемы менять не нужно — они провайдер-агностичны. - -## Roadmap - -- Фаза 2: веб-админка (отдельный пакет `admin`, Hono/Fastify + лёгкий фронт) на общем `core`: галерея контента, дашборд трат, управление провайдерами. -- Доп. провайдеры: RouterAI, GPTunnel, OpenRouter; облачный STT. diff --git a/NexusAI/TEST_RESUME.md b/NexusAI/TEST_RESUME.md deleted file mode 100644 index cda6539..0000000 --- a/NexusAI/TEST_RESUME.md +++ /dev/null @@ -1,372 +0,0 @@ -# NexusAI — полное резюме тестов - -**Дата:** 2026-08-18 -**Репозиторий:** `/home/user/moder/mcp/NexusAI` -**Ветка:** `main` -**Админка:** http://127.0.0.1:4123 - ---- - -## 1. Общая архитектура - -Проект — универсальный MCP-сервис для агентов: - -- `packages/core` — журнал SQLite, шифрование ключей, медиа-хранилище, провайдеры. -- `packages/mcp-server` — MCP stdio-сервер с инструментами `nexus_*`. -- `packages/admin` — Fastify web UI (дашборд, галерея, управление провайдерами). -- `sidecar/stt_server.py` — локальный faster-whisper sidecar. - -Провайдер по умолчанию — **NordRouter** (`https://nordrouter.com`). -Ключи хранятся в SQLite, зашифрованные мастер-ключом `NEXUS_MASTER_KEY`. - -### Переменные окружения, использованные в тестах - -```bash -NEXUS_MEDIA_DIR=/tmp/opencode/nexus-media -NEXUS_MASTER_KEY=fixed-test-master-key-1234567890 -NEXUS_ADMIN_PORT=4123 -NEXUS_STT_MODEL=/tmp/opencode/whisper-tiny -NEXUS_STT_LOCAL_ONLY=1 -NEXUS_STT_DEVICE=cpu -HF_ENDPOINT=https://hf-mirror.com -``` - ---- - -## 2. Запуск админки - -Админ-сервер поднимался в detached `tmux`-сессии, потому что фоновые процессы в bash-инструменте реапятся. - -```bash -tmux kill-session -t nexusadmin 2>/dev/null; sleep 1 -tmux new-session -d -s nexusadmin -c /home/user/moder/mcp/NexusAI \ - "export NEXUS_MEDIA_DIR=/tmp/opencode/nexus-media NEXUS_MASTER_KEY=fixed-test-master-key-1234567890 NEXUS_ADMIN_PORT=4123; exec node packages/admin/dist/server.js >/tmp/opencode/admin_live.log 2>&1" -``` - -**Проверка:** - -```text -HTTP:200 -[2026-08-18T02:59:15.659Z] INFO (admin:api) routes registered -[2026-08-18T02:59:15.688Z] INFO (admin) NexusAI admin running at http://127.0.0.1:4123 -``` - ---- - -## 3. Тест: веб-поиск «последние новости Барнаула» - -### Скрипт - -Временный: `packages/mcp-server/_search_test.cjs` - -### Вызов - -```javascript -const { buildContext } = require('./dist/context.js'); -const ctx = buildContext(); -const p = ctx.registry.getSearch('nordrouter'); -const res = await p.search({ query: 'последние новости Барнаула', recency: 'day', maxResults: 5 }); -``` - -### Статус - -✅ Успех. - -### Сырой ответ ( answer ) - -```text -Вот самые заметные новости Барнаула за последние сутки по доступным заголовкам и лентам: - -- В Барнауле поддерживают подготовку к отопительному сезону... -- В городе идут дожди... -- В Барнауле можно бесплатно оформить медкарты... -- Показательные выступления служебных собак... -- На этой неделе в Барнауле можно сделать флюорографию... -- ... -``` - -### Citations - -1. https://brl.mk.ru/news/ -2. https://ria.ru/location_Barnaul/ -3. https://www.alt.kp.ru/ -4. https://tolknews.ru/ -5. https://barnaul.org/news/ - -### Примечание - -- Вызов был напрямую через адаптер, не через MCP-инструмент, поэтому в журнал не записался. -- `costUsd` вернулся `undefined`: NordRouter не возвращает `usage.total_cost` для `perplexity/sonar-pro`. - ---- - -## 4. Тест: STT → TTS - -### 4.1 Подготовка STT-модели - -HuggingFace был недоступен (`Broken pipe`), поэтому модель `faster-whisper-tiny` скачана с зеркала ModelScope: - -```bash -mkdir -p /tmp/opencode/whisper-tiny -cd /tmp/opencode/whisper-tiny -for f in config.json configuration.json model.bin tokenizer.json vocabulary.txt README.md; do - curl -sL -o "$f" "https://modelscope.cn/models/systran/faster-whisper-tiny/resolve/master/$f" -done -``` - -### 4.2 Скрипты - -- `packages/mcp-server/_stt_test.cjs` — прямой вызов `local` STT. -- `packages/mcp-server/_tts_test.cjs` — прямой вызов NordRouter TTS. - -### 4.3 STT-результат - -**Файл:** `/home/user/Музыка/Денис/audio_2025-08-21_12-21-30.ogg` (30K, Ogg) - -```text ---- text --- -А то с ука в 30 градусной жарее весь трещал передел свистелблять. Каких там только звуков не было под нагрузкой блять. - -language: ru | duration: 7.76 -``` - -### 4.4 TTS-результат - -Модель: `audio/gemini-3.1-flash-tts`, голос `Zephyr`. - -```text -file_path: /tmp/opencode/nexus-media/audio-2026-08-18T03-25-00-156Z-v50yby.wav -mime_type: audio/wav -result_url: https://nordrouter.com/media/file/f987fdf8-5e2b-42c8-9883-6f18f8764452 -cost_usd: 0.00825 -``` - -### 4.5 Позже: полный цикл через MCP-инструменты - -Использован `Handlers.handle('nexus_stt', ...)` и `Handlers.handle('nexus_tts', ...)`. -Записи появились в журнале и галерее. - -**Фикс:** `nexus_stt` больше не сохраняет путь к исходному аудио как `filePath`, иначе галерея пытается отдать файл из `/home/user/Музыка/...` через `/media/` и получает 404. - -Коммит: `c65f910`. - ---- - -## 5. Тест: генерация изображений (GPT Image 2, Seedream 5.0 Pro, Gemini 3.1 Flash) - -### Скрипты - -- `packages/mcp-server/_image_models.cjs` — список image-моделей. -- `packages/mcp-server/_image_gen.cjs` — генерация тремя моделями. - -### Промпт - -```text -лисица кусает мухамор реалистичное фото изображение сделанное на проффисианальную камеру -``` - -### Результаты - -| Модель | Статус | Файл | Стоимость | -|---|---|---|---| -| `image/gpt-image-2` | ✅ done | `/tmp/opencode/nexus-media/image-2026-08-18T04-04-28-304Z-izb5rj.png` | `$0` | -| `image/gemini-3.1-flash` | ✅ done | `/tmp/opencode/nexus-media/image-2026-08-18T04-04-48-360Z-3gloqp.png` | `$0.011534` | -| `image/seedream-5.0-pro` | ❌ failed | — | средства не списаны | - -### Ошибка Seedream - -```text -NexusError: Генерация временно недоступна — попробуйте ещё раз. Средства не списаны. -``` - -Повторный запрос дал тот же результат. - -### Сырой ответ (Gemini 3.1 Flash) - -Изображение: лиса в зелёном лесу с красным мухомором во рту. - ---- - -## 6. Тест: видео из картинки (Seedance 1.5 Pro) - -### Скрипты - -- `packages/mcp-server/_video_models.cjs` -- `packages/mcp-server/_video_gen.cjs` - -### Параметры - -- Модель: `video/seedance-1.5-pro` -- Входное изображение: `https://nordrouter.com/media/file/4da49a3c-3866-44af-8e6c-d9360f5ebab8` -- Промпт: `A red fox gently bites and eats a bright red fly agaric mushroom...` -- `duration=5`, `aspect_ratio=16:9`, `resolution=720p`, `generate_audio=true` - -### Результат - -```text -status: done -file_path: /tmp/opencode/nexus-media/video-2026-08-18T04-18-15-277Z-jb2tk9.mp4 -mime_type: video/mp4 -result_url: https://nordrouter.com/media/file/3dc0b9e7-5889-4f7b-b431-9087906380f9 -cost_usd: 0.21875 -``` - -Видео: 720p, 24 fps, AAC stereo — со звуком. - ---- - -## 7. Тест: Kling 2.6 i2v с vision-описанием - -### 7.1 Добавлен инструмент `nexus_vision` - -Для полного цикла через NexusAI добавлен MCP-инструмент `nexus_vision`. - -Изменения: - -- `packages/mcp-server/src/schemas.ts` — `VisionSchema` -- `packages/mcp-server/src/handlers.ts` — обработчик `vision` -- `packages/mcp-server/src/tools.ts` — регистрация инструмента -- `packages/core/src/shared/http.ts` — поддержка `FormData` -- `packages/core/src/providers/nordrouter/media.ts` — определение MIME по расширению для upload - -Дефолтная vision-модель: `google/gemini-3.1-flash-lite`. - -### 7.2 Скрипты - -- `packages/mcp-server/_video_models.cjs` -- `packages/mcp-server/_vision_model_test.cjs` — подбор рабочей vision-модели -- `packages/mcp-server/_upload_image.cjs`, `_upload_debug.cjs`, `_upload_json.cjs`, `_upload_dataurl.cjs` — отладка upload -- `packages/mcp-server/_kling_test.cjs`, `_kling_retry.cjs`, `_kling_base64.cjs`, `_kling_neutral.cjs`, `_kling_standard.cjs`, `_kling26.cjs`, `_kling26_uploaded.cjs` — тесты Kling - -### 7.3 Подбор vision-модели - -Начальные попытки (`gpt-4o`, `openai/gpt-4o`, `gemini-3.1-flash`) вернули `unknown model`. -Список `/v1/models` показал, что у NordRouter модели вида `google/gemini-3.1-flash-lite`, `openai/gpt-5.4-mini` и т.д. - -Рабочая: `google/gemini-3.1-flash-lite`. - -### 7.4 Vision-описание (сырой ответ) - -```text -Вот подробное описание сцены, структурированное как профессиональный промпт для генерации видео: - -### Видео-промпт: «Лиса и мухомор в осеннем лесу» - -**Общая сцена и композиция:** -Кинематографичный кадр крупным планом, уровень глаз животного... - -**Детали объектов:** -* Лиса: густая пушистая шерсть рыжего цвета... -* Гриб: классический мухомор... -* Окружение: лесная подстилка, мох, опавшие листья... - -**Освещение и цвета:** -Мягкий естественный дневной свет... - -**Технические параметры:** -Стиль фотоработы National Geographic, 8k, макро-фотография, гиперреализм... -Камера: f/2.8, объектив 85 мм... -``` - -### 7.5 Проблемы с Kling - -Сначала Kling 2.1/2.6 возвращали: - -```text -Не удалось сгенерировать. Средства возвращены на баланс. -``` - -Причина: Kling i2v не принимал прямую ссылку на изображение, сгенерированное через `/media/generate`. -Решение: загрузить исходник через `nexus_media_upload` и получить URL вида `https://nordrouter.com/media/file/up-...?raw=1`. - -Также при upload был баг: MIME определялся как `application/octet-stream`, и NordRouter отвечал `HTTP 415`. Починено определением MIME по расширению файла. - -### 7.6 Результат Kling 2.6 i2v - -```text -{ - "provider": "nordrouter", - "capability": "video", - "model": "video/kling-2.6-i2v", - "status": "done", - "file_path": "/tmp/opencode/nexus-media/video-2026-08-18T04-48-21-385Z-r5um7w.mp4", - "mime_type": "video/mp4", - "result_url": "https://nordrouter.com/media/file/3274f543-b954-4db5-9407-975cfa4de7cd", - "cost_usd": 0.6875 -} -``` - -- Длительность: 5.04 сек -- Разрешение: 1440×1440, 24 fps -- Аудио: AAC 44.1 kHz stereo -- Размер: 13M - ---- - -## 8. Ошибки и фиксы - -| Проблема | Причина | Решение | Коммит | -|---|---|---|---| -| MCP падает без `NEXUS_MASTER_KEY` | `KeyCipher` пытался расшифровать пустой ключ | graceful fallback | `6ff2b27` | -| STT-запись даёт битую ссылку в галерее | `filePath` указывал на исходный аудиофайл | `filePath = null` для STT | `c65f910` | -| Upload возвращает 415 | MIME не определялся по расширению | таблица `mimeByExt` | `b2f57cc` | -| Нет инструмента для vision | не было MCP-инструмента | добавлен `nexus_vision` | `b2f57cc` | -| Kling i2v не принимал сгенерированный URL | требует upload-URL `?raw=1` | использован `nexus_media_upload` | — | - ---- - -## 9. Итоговая статистика - -```json -{ - "totalCostUsd": 0.950597, - "totalCount": 18, - "byProvider": [ - { "provider": "nordrouter", "count": 17, "costUsd": 0.950597 }, - { "provider": "local", "count": 1, "costUsd": 0 } - ], - "byCapability": [ - { "capability": "video", "count": 2, "costUsd": 0.90625 }, - { "capability": "tts", "count": 4, "costUsd": 0.022813 }, - { "capability": "image", "count": 4, "costUsd": 0.021534 }, - { "capability": "vision", "count": 1, "costUsd": 0 }, - { "capability": "stt", "count": 1, "costUsd": 0 } - ], - "byDay": [ - { "day": "2026-08-18", "count": 14, "costUsd": 0.931534 }, - { "day": "2026-08-17", "count": 4, "costUsd": 0.019063 } - ] -} -``` - ---- - -## 10. Коммиты - -```text -b2f57cc feat(mcp-server): add nexus_vision tool and fix upload MIME detection -c65f910 fix(mcp-server): do not store source audio path as STT gallery file -1bc3746 Admin UX: light/dark theme, gallery pagination, content modal + delete, openai type -6ff2b27 Fix MCP crash when DB key exists without NEXUS_MASTER_KEY -679e564 Add admin web UI (Phase 2): spend dashboard, media gallery, provider management -``` - ---- - -## 11. Ключевые файлы - -- `/tmp/opencode/nexus-media/image-2026-08-18T04-04-48-360Z-3gloqp.png` — Gemini 3.1 Flash (исходник для видео) -- `/tmp/opencode/nexus-media/video-2026-08-18T04-18-15-277Z-jb2tk9.mp4` — Seedance 1.5 Pro i2v (со звуком) -- `/tmp/opencode/nexus-media/video-2026-08-18T04-48-21-385Z-r5um7w.mp4` — Kling 2.6 i2v (со звуком) -- `/tmp/opencode/whisper-tiny/` — локальная STT-модель -- `/tmp/opencode/admin_live.log` — лог админки - ---- - -## 12. Выводы - -- ✅ MCP-инструменты работают в полном цикле и пишут в журнал/галерею. -- ✅ Web search, STT, TTS, image, video, vision — все интегрированы. -- ✅ Поддержка конкретных моделей (`model` в запросе) работает. -- ⚠️ Некоторые модели временно недоступны (`seedream-5.0-pro`) или требуют upload (`kling-2.6-i2v`). -- ⚠️ Расходы на видео значительно выше оценок: Kling 2.6 i2v `$0.6875` против estimated `$0.3438`. diff --git a/NexusAI/package-lock.json b/NexusAI/package-lock.json deleted file mode 100644 index 0393b35..0000000 --- a/NexusAI/package-lock.json +++ /dev/null @@ -1,2391 +0,0 @@ -{ - "name": "nexusai", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nexusai", - "version": "0.1.0", - "license": "MIT", - "workspaces": [ - "packages/core", - "packages/mcp-server", - "packages/admin" - ] - }, - "node_modules/@fastify/accept-negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", - "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@fastify/ajv-compiler": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", - "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", - "dependencies": { - "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", - "fast-uri": "^2.0.0" - } - }, - "node_modules/@fastify/ajv-compiler/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@fastify/ajv-compiler/node_modules/fast-uri": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.4.tgz", - "integrity": "sha512-GntYZbd2KSiFfoZI3Y02rXKihfsPwdWfiHrwKVLuU1i810D0SYw7fCarLxaRO2VvneTrbzCxSz3GnvEfUiApug==" - }, - "node_modules/@fastify/error": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", - "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", - "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", - "dependencies": { - "fast-json-stringify": "^5.7.0" - } - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", - "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/@fastify/send": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", - "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", - "dependencies": { - "@lukeed/ms": "^2.0.1", - "escape-html": "~1.0.3", - "fast-decode-uri-component": "^1.0.1", - "http-errors": "2.0.0", - "mime": "^3.0.0" - } - }, - "node_modules/@fastify/send/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@fastify/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@fastify/static": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz", - "integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==", - "dependencies": { - "@fastify/accept-negotiator": "^1.0.0", - "@fastify/send": "^2.0.0", - "content-disposition": "^0.5.3", - "fastify-plugin": "^4.0.0", - "fastq": "^1.17.0", - "glob": "^10.3.4" - } - }, - "node_modules/@fastify/static/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==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@lukeed/ms": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", - "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", - "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "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/@nexusai/admin": { - "resolved": "packages/admin", - "link": true - }, - "node_modules/@nexusai/core": { - "resolved": "packages/core", - "link": true - }, - "node_modules/@nexusai/mcp-server": { - "resolved": "packages/mcp-server", - "link": true - }, - "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==" - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "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, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "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==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "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==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/avvio": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", - "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", - "dependencies": { - "@fastify/error": "^3.3.0", - "fastq": "^1.17.1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "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" - } - ] - }, - "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, - "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==", - "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==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "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" - } - ], - "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==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "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==", - "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==", - "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==", - "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==", - "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==", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/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==", - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", - "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", - "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==", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", - "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-content-type-parse": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", - "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" - }, - "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==" - }, - "node_modules/fast-json-stringify": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", - "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", - "dependencies": { - "@fastify/merge-json-schemas": "^0.1.0", - "ajv": "^8.10.0", - "ajv-formats": "^3.0.1", - "fast-deep-equal": "^3.1.3", - "fast-uri": "^2.1.0", - "json-schema-ref-resolver": "^1.0.1", - "rfdc": "^1.2.0" - } - }, - "node_modules/fast-json-stringify/node_modules/fast-uri": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.4.tgz", - "integrity": "sha512-GntYZbd2KSiFfoZI3Y02rXKihfsPwdWfiHrwKVLuU1i810D0SYw7fCarLxaRO2VvneTrbzCxSz3GnvEfUiApug==" - }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "node_modules/fastify": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", - "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "dependencies": { - "@fastify/ajv-compiler": "^3.5.0", - "@fastify/error": "^3.4.0", - "@fastify/fast-json-stringify-compiler": "^4.3.0", - "abstract-logging": "^2.0.1", - "avvio": "^8.3.0", - "fast-content-type-parse": "^1.1.0", - "fast-json-stringify": "^5.8.0", - "find-my-way": "^8.0.0", - "light-my-request": "^5.11.0", - "pino": "^9.0.0", - "process-warning": "^3.0.0", - "proxy-addr": "^2.0.7", - "rfdc": "^1.3.0", - "secure-json-parse": "^2.7.0", - "semver": "^7.5.4", - "toad-cache": "^3.3.0" - } - }, - "node_modules/fastify-plugin": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", - "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "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==" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "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/find-my-way": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", - "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^3.1.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/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==" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/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==" - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz", - "integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==", - "engines": { - "node": ">=16.9.0" - } - }, - "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==", - "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.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "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" - } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/ip-address": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", - "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", - "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==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jose": { - "version": "6.2.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", - "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-ref-resolver": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", - "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "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==" - }, - "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==" - }, - "node_modules/light-my-request": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", - "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", - "dependencies": { - "cookie": "^0.7.0", - "process-warning": "^3.0.0", - "set-cookie-parser": "^2.4.1" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "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==" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/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==" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.94.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", - "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-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==", - "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==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "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==", - "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==" - }, - "node_modules/pino/node_modules/process-warning": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", - "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "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==", - "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.", - "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": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==" - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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==", - "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==", - "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==", - "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==", - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ret": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", - "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex2": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", - "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", - "dependencies": { - "ret": "~0.4.0" - } - }, - "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==", - "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==" - }, - "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==" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "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/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "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.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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" - } - ] - }, - "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" - } - ], - "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==", - "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==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "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==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", - "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", - "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==", - "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.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", - "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", - "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", - "engines": { - "node": ">=20" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "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, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "packages/admin": { - "version": "0.1.0", - "dependencies": { - "@fastify/static": "^7.0.0", - "@nexusai/core": "*", - "fastify": "^4.28.0" - }, - "bin": { - "nexusai-admin": "dist/server.js" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } - }, - "packages/core": { - "name": "@nexusai/core", - "version": "0.1.0", - "dependencies": { - "better-sqlite3": "^11.8.0", - "undici": "^6.27.0", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.0", - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } - }, - "packages/mcp-server": { - "name": "@nexusai/mcp-server", - "version": "0.1.0", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.0", - "@nexusai/core": "*", - "zod": "^3.25.0", - "zod-to-json-schema": "^3.25.0" - }, - "bin": { - "nexusai-mcp": "dist/index.js" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } - } - } -} diff --git a/NexusAI/package.json b/NexusAI/package.json deleted file mode 100644 index 62bf8aa..0000000 --- a/NexusAI/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "nexusai", - "version": "0.1.0", - "private": true, - "description": "Universal MCP service for agents: TTS, STT, image/video/music generation and web search", - "workspaces": [ - "packages/core", - "packages/mcp-server", - "packages/admin" - ], - "scripts": { - "build": "npm run build --workspace @nexusai/core && npm run build --workspace @nexusai/mcp-server && npm run build --workspace @nexusai/admin", - "build:core": "npm run build --workspace @nexusai/core", - "build:mcp": "npm run build --workspace @nexusai/mcp-server", - "build:admin": "npm run build --workspace @nexusai/admin", - "start": "node packages/mcp-server/dist/index.js", - "admin": "node packages/admin/dist/server.js", - "dev": "npm run build && node packages/mcp-server/dist/index.js" - }, - "license": "MIT" -} diff --git a/NexusAI/packages/admin/package.json b/NexusAI/packages/admin/package.json deleted file mode 100644 index 3e82a0f..0000000 --- a/NexusAI/packages/admin/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@nexusai/admin", - "version": "0.1.0", - "description": "NexusAI admin: web dashboard for content gallery, spend tracking and provider/key management", - "main": "dist/server.js", - "type": "commonjs", - "bin": { - "nexusai-admin": "dist/server.js" - }, - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "start": "node dist/server.js" - }, - "dependencies": { - "@fastify/static": "^7.0.0", - "@nexusai/core": "*", - "fastify": "^4.28.0" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } -} diff --git a/NexusAI/packages/admin/public/app.js b/NexusAI/packages/admin/public/app.js deleted file mode 100644 index 7fbeecc..0000000 --- a/NexusAI/packages/admin/public/app.js +++ /dev/null @@ -1,328 +0,0 @@ -'use strict'; - -const $ = (sel) => document.querySelector(sel); -const $$ = (sel) => Array.from(document.querySelectorAll(sel)); - -function toast(msg) { - const el = document.createElement('div'); - el.className = 'toast'; - el.textContent = msg; - document.body.appendChild(el); - setTimeout(() => el.remove(), 2600); -} - -async function api(path, opts) { - const res = await fetch(path, { - headers: { 'Content-Type': 'application/json' }, - ...opts, - }); - const text = await res.text(); - let data = null; - try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; } - if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`); - return data; -} - -function usd(n) { return (n == null ? 0 : n).toFixed(4); } -function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => - ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); } - -// --- Theme --- -function applyTheme(theme) { - document.documentElement.setAttribute('data-theme', theme); - const btn = $('#themeToggle'); - btn.textContent = theme === 'dark' ? '🌙 Тёмная' : '☀️ Светлая'; - localStorage.setItem('nexus-theme', theme); -} -$('#themeToggle').addEventListener('click', () => { - const cur = document.documentElement.getAttribute('data-theme'); - applyTheme(cur === 'dark' ? 'light' : 'dark'); -}); -applyTheme(localStorage.getItem('nexus-theme') || 'dark'); - -// --- Tabs --- -let activeTab = 'usage'; -function refreshActiveTab() { - if (activeTab === 'usage') return loadStats(); - if (activeTab === 'gallery') return loadGallery(); - if (activeTab === 'providers') return loadProviders(); -} -$$('nav button').forEach((btn) => { - btn.addEventListener('click', () => { - $$('nav button').forEach((b) => b.classList.remove('active')); - btn.classList.add('active'); - $$('main section').forEach((s) => s.classList.add('hidden')); - $(`#tab-${btn.dataset.tab}`).classList.remove('hidden'); - activeTab = btn.dataset.tab; - refreshActiveTab(); - }); -}); - -// --- Auto-refresh (no page reload) --- -let autoTimer = null; -function isModalOpen() { return $('#modalRoot').innerHTML.trim() !== ''; } -function setupAutoRefresh() { - if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } - const on = $('#autoRefresh').checked; - localStorage.setItem('nexus-autorefresh', on ? '1' : '0'); - localStorage.setItem('nexus-autorefresh-int', $('#autoRefreshInterval').value); - if (!on) return; - const ms = parseInt($('#autoRefreshInterval').value, 10) || 10000; - autoTimer = setInterval(() => { - // Don't disrupt the user while a modal is open or an audio is playing. - if (isModalOpen()) return; - if (document.hidden) return; - refreshActiveTab(); - }, ms); -} -$('#autoRefresh').checked = localStorage.getItem('nexus-autorefresh') === '1'; -$('#autoRefreshInterval').value = localStorage.getItem('nexus-autorefresh-int') || '10000'; -$('#autoRefresh').addEventListener('change', setupAutoRefresh); -$('#autoRefreshInterval').addEventListener('change', setupAutoRefresh); -setupAutoRefresh(); - -// --- Usage --- -async function loadStats() { - const range = $('#statsRange').value; - const q = range ? `?sinceDays=${range}` : ''; - const s = await api(`/api/stats${q}`); - $('#statCards').innerHTML = ` -
$${usd(s.totalCostUsd)}
Всего потрачено
-
${s.totalCount}
Всего операций
-
${s.byProvider.length}
Провайдеров
-
${s.byCapability.length}
Типов операций
`; - const fill = (id, rows, key) => { - $(`#${id} tbody`).innerHTML = rows.map((r) => - `${esc(r[key])}${r.count}$${usd(r.costUsd)}`).join('') - || 'Нет данных'; - }; - fill('byProvider', s.byProvider, 'provider'); - fill('byCapability', s.byCapability, 'capability'); - fill('byDay', s.byDay, 'day'); -} -$('#statsRange').addEventListener('change', loadStats); - -// --- Gallery (with pagination) --- -let galleryOffset = 0; -let galleryLastCount = 0; - -function pageSize() { return parseInt($('#pageSize').value, 10) || 50; } - -async function loadGallery() { - const cap = $('#galleryFilter').value; - const limit = pageSize(); - const params = new URLSearchParams(); - params.set('limit', String(limit)); - params.set('offset', String(galleryOffset)); - if (cap) params.set('capability', cap); - const { items } = await api(`/api/generations?${params.toString()}`); - galleryLastCount = items.length; - $('#gallery').innerHTML = items.map(renderTile).join('') - || '
Пока нет сгенерированного контента.
'; - const page = Math.floor(galleryOffset / limit) + 1; - $('#pageInfo').textContent = `Страница ${page} · показано ${items.length}`; - $('#prevPage').disabled = galleryOffset === 0; - $('#nextPage').disabled = items.length < limit; - - $$('[data-view]').forEach((el) => el.onclick = () => openModal(el.dataset.view)); - $$('[data-del]').forEach((b) => b.onclick = (e) => { e.stopPropagation(); delGeneration(b.dataset.del); }); - $$('[data-play]').forEach((b) => b.onclick = (e) => { e.stopPropagation(); openModal(b.dataset.play); }); -} - -function isAudio(it) { return it.capability === 'tts' || it.capability === 'music'; } -function isText(it) { return it.capability === 'stt' || (it.mediaUrl && /\.md$/i.test(it.mediaUrl)) || it.capability === 'vision'; } - -function renderTile(it) { - const cost = it.costUsd != null ? `$${usd(it.costUsd)}` : ''; - let media = ''; - if (it.mediaUrl) { - if (it.capability === 'image') { - media = `
`; - } else if (isAudio(it)) { - media = `
- -
`; - } else if (it.capability === 'video') { - media = `
`; - } else if (isText(it)) { - media = `
📄
`; - } else { - media = `Открыть файл`; - } - } else { - const status = it.status === 'failed' ? '⚠ ошибка' : 'нет локального файла'; - media = `
- ${status}
`; - } - return `
- - ${media} -
-
${esc(it.capability)} · ${cost}
-
${esc(it.model)}
-
${new Date(it.createdAt).toLocaleString()}
-
-
`; -} - -$('#galleryReload').addEventListener('click', () => { galleryOffset = 0; loadGallery(); }); -$('#galleryFilter').addEventListener('change', () => { galleryOffset = 0; loadGallery(); }); -$('#pageSize').addEventListener('change', () => { galleryOffset = 0; loadGallery(); }); -$('#prevPage').addEventListener('click', () => { - galleryOffset = Math.max(0, galleryOffset - pageSize()); - loadGallery(); -}); -$('#nextPage').addEventListener('click', () => { - if (galleryLastCount < pageSize()) return; - galleryOffset += pageSize(); - loadGallery(); -}); - -// --- Modal viewer --- -async function openModal(id) { - let rec; - try { rec = await api(`/api/generations/${encodeURIComponent(id)}`); } - catch (e) { return toast('Ошибка: ' + e.message); } - - let body = ''; - if (rec.mediaUrl && rec.capability === 'image') { - body = ``; - } else if (rec.mediaUrl && rec.capability === 'video') { - body = ``; - } else if (rec.mediaUrl && (rec.capability === 'tts' || rec.capability === 'music')) { - body = `
-
🎵
- -
`; - } else if (rec.mediaUrl && /\.md$/i.test(rec.mediaUrl)) { - // Transcript / text document — fetch and render. - let text = ''; - try { text = await (await fetch(rec.mediaUrl)).text(); } catch { text = '(не удалось загрузить текст)'; } - body = `
${esc(text)}
`; - } else { - body = `
Нет предпросмотра для этого типа.
`; - } - - const kv = ` -
Модель: ${esc(rec.model)}
-
Провайдер: ${esc(rec.provider)} · Возможность: ${esc(rec.capability)}
-
Стоимость: ${rec.costUsd != null ? '$' + usd(rec.costUsd) : '—'}
-
Создано: ${new Date(rec.createdAt).toLocaleString()}
- ${rec.inputSummary ? `
Вход: ${esc(rec.inputSummary)}
` : ''} - ${rec.error ? `
Ошибка: ${esc(rec.error)}
` : ''}`; - - $('#modalRoot').innerHTML = ` - `; - - const close = () => { $('#modalRoot').innerHTML = ''; }; - $('#modalClose').onclick = close; - $('#modalBackdrop').onclick = (e) => { if (e.target.id === 'modalBackdrop') close(); }; - document.addEventListener('keydown', function onEsc(e) { - if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onEsc); } - }); - $('#modalDel').onclick = async () => { - if (!confirm('Удалить этот контент?')) return; - await delGeneration(id, true); - close(); - }; -} - -async function delGeneration(id, silent) { - if (!silent && !confirm('Удалить этот контент?')) return; - try { - await api(`/api/generations/${encodeURIComponent(id)}`, { method: 'DELETE' }); - toast('Удалено'); - loadGallery(); - } catch (e) { toast('Ошибка: ' + e.message); } -} - -// --- Providers --- -let CRYPTO_ENABLED = true; -async function loadProviders() { - const { providers, knownTypes, cryptoEnabled } = await api('/api/providers'); - CRYPTO_ENABLED = cryptoEnabled; - $('#cryptoBadge').textContent = cryptoEnabled - ? 'Шифрование ключей: включено' - : '⚠ NEXUS_MASTER_KEY не задан — ключи из БД недоступны'; - const typeSel = $('#pType'); - if (!typeSel.options.length) { - typeSel.innerHTML = knownTypes.map((t) => ``).join(''); - } - $('#providersTable tbody').innerHTML = providers.map((p) => { - const status = [ - p.isDefault ? 'default' : '', - p.enabled ? 'включён' : 'выключен', - ].join(' '); - return ` - ${esc(p.name)} - ${esc(p.type)} - ${p.hasKey ? `${esc(p.keyHint || '••••')}` : 'нет'} - ${status} - - - - - - - `; - }).join('') || 'Нет провайдеров. Добавьте выше.'; - - $$('[data-check]').forEach((b) => b.onclick = () => checkProvider(b.dataset.check)); - $$('[data-toggle]').forEach((b) => b.onclick = () => toggleProvider(b.dataset.toggle, b.dataset.en !== 'true')); - $$('[data-default]').forEach((b) => b.onclick = () => setDefault(b.dataset.default)); - $$('[data-del-provider]').forEach((b) => b.onclick = () => delProvider(b.dataset.delProvider)); -} - -$('#pSave').addEventListener('click', async () => { - try { - const body = { - name: $('#pName').value.trim(), - type: $('#pType').value, - baseUrl: $('#pBaseUrl').value.trim() || undefined, - apiKey: $('#pKey').value.trim() || undefined, - isDefault: $('#pDefault').value === 'true', - enabled: $('#pEnabled').value === 'true', - }; - if (!body.name) return toast('Укажите имя'); - if (body.apiKey && !CRYPTO_ENABLED) return toast('Нельзя сохранить ключ без NEXUS_MASTER_KEY'); - await api('/api/providers', { method: 'POST', body: JSON.stringify(body) }); - $('#pKey').value = ''; - toast('Сохранено'); - loadProviders(); - } catch (e) { toast('Ошибка: ' + e.message); } -}); - -async function checkProvider(name) { - try { - toast('Проверяю ключ...'); - const r = await api(`/api/providers/${encodeURIComponent(name)}/check`, { method: 'POST', body: '{}' }); - toast(r.ok ? `OK · моделей: ${r.modelsCount}` : `Ошибка ключа: ${r.error}`); - } catch (e) { toast('Ошибка: ' + e.message); } -} -async function toggleProvider(name, enabled) { - try { await api(`/api/providers/${encodeURIComponent(name)}/enabled`, { method: 'POST', body: JSON.stringify({ enabled }) }); loadProviders(); } - catch (e) { toast('Ошибка: ' + e.message); } -} -async function setDefault(name) { - try { await api(`/api/providers/${encodeURIComponent(name)}/default`, { method: 'POST', body: '{}' }); loadProviders(); } - catch (e) { toast('Ошибка: ' + e.message); } -} -async function delProvider(name) { - if (!confirm(`Удалить провайдера "${name}"?`)) return; - try { await api(`/api/providers/${encodeURIComponent(name)}`, { method: 'DELETE' }); loadProviders(); } - catch (e) { toast('Ошибка: ' + e.message); } -} - -// initial -loadStats(); diff --git a/NexusAI/packages/admin/public/index.html b/NexusAI/packages/admin/public/index.html deleted file mode 100644 index 22e4d75..0000000 --- a/NexusAI/packages/admin/public/index.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - NexusAI Admin - - - -
-

NexusAI Admin

- - - - -
- -
-
-
-
-
- - -
-
-
-
-
-

По провайдерам

-
ПровайдерКол-воСтоимость, $
-
-
-

По возможностям

-
ВозможностьКол-воСтоимость, $
-
-
-

По дням

-
ДеньКол-воСтоимость, $
-
-
- - - - -
- -
- - - diff --git a/NexusAI/packages/admin/src/context.ts b/NexusAI/packages/admin/src/context.ts deleted file mode 100644 index fea2e1d..0000000 --- a/NexusAI/packages/admin/src/context.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - Journal, - KeyCipher, - ProvidersStore, - loadConfig, - createLogger, - type NexusConfig, -} from '@nexusai/core'; - -const log = createLogger('admin:context'); - -export interface AdminContext { - config: NexusConfig; - journal: Journal; - store: ProvidersStore; - cipher: KeyCipher; -} - -export function buildAdminContext(): AdminContext { - const config = loadConfig(); - const journal = new Journal(config.dbPath); - const cipher = new KeyCipher(config.masterKey); - const store = new ProvidersStore(config.providersDbPath, cipher); - - if (!cipher.enabled) { - log.warn('NEXUS_MASTER_KEY not set — provider keys cannot be added/decrypted from the admin'); - } - return { config, journal, store, cipher }; -} diff --git a/NexusAI/packages/admin/src/routes.ts b/NexusAI/packages/admin/src/routes.ts deleted file mode 100644 index 957dcd4..0000000 --- a/NexusAI/packages/admin/src/routes.ts +++ /dev/null @@ -1,193 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import type { FastifyInstance } from 'fastify'; -import { - NexusError, - NordRouterClient, - NordRouterMediaProvider, - MediaStorage, - createLogger, -} from '@nexusai/core'; -import type { AdminContext } from './context.js'; - -const log = createLogger('admin:api'); - -const KNOWN_TYPES = ['nordrouter', 'openai']; - -function errorReply(err: unknown): { statusCode: number; body: { error: string; code: string } } { - const e = NexusError.from(err); - const statusCode = - e.code === 'NOT_FOUND' ? 404 : e.code === 'CONFIG' || e.code === 'BAD_REQUEST' ? 400 : 500; - return { statusCode, body: { error: e.message, code: e.code } }; -} - -export function registerRoutes(app: FastifyInstance, ctx: AdminContext): void { - // --- Providers --- - app.get('/api/providers', async () => { - return { providers: ctx.store.listViews(), knownTypes: KNOWN_TYPES, cryptoEnabled: ctx.cipher.enabled }; - }); - - app.post('/api/providers', async (req, reply) => { - try { - const body = req.body as { - name?: string; - type?: string; - baseUrl?: string; - apiKey?: string; - enabled?: boolean; - isDefault?: boolean; - }; - if (!body?.name || !body?.type) { - throw new NexusError('BAD_REQUEST', 'name and type are required'); - } - if (body.apiKey && !ctx.cipher.enabled) { - throw new NexusError('CONFIG', 'NEXUS_MASTER_KEY must be set to store an API key'); - } - ctx.store.upsert({ - name: body.name, - type: body.type, - baseUrl: body.baseUrl ?? null, - apiKey: body.apiKey, - enabled: body.enabled, - isDefault: body.isDefault, - }); - return { ok: true, provider: ctx.store.get(body.name) }; - } catch (err) { - const { statusCode, body } = errorReply(err); - return reply.code(statusCode).send(body); - } - }); - - app.post('/api/providers/:name/enabled', async (req, reply) => { - try { - const { name } = req.params as { name: string }; - const { enabled } = req.body as { enabled: boolean }; - ctx.store.setEnabled(name, enabled); - return { ok: true }; - } catch (err) { - const { statusCode, body } = errorReply(err); - return reply.code(statusCode).send(body); - } - }); - - app.post('/api/providers/:name/default', async (req, reply) => { - try { - const { name } = req.params as { name: string }; - ctx.store.setDefault(name); - return { ok: true }; - } catch (err) { - const { statusCode, body } = errorReply(err); - return reply.code(statusCode).send(body); - } - }); - - app.delete('/api/providers/:name', async (req, reply) => { - try { - const { name } = req.params as { name: string }; - ctx.store.remove(name); - return { ok: true }; - } catch (err) { - const { statusCode, body } = errorReply(err); - return reply.code(statusCode).send(body); - } - }); - - // Validate a key: uses provided key, or the stored one, or env fallback. - app.post('/api/providers/:name/check', async (req, reply) => { - try { - const { name } = req.params as { name: string }; - const body = (req.body ?? {}) as { apiKey?: string; baseUrl?: string }; - const row = ctx.store.get(name); - const type = row?.type ?? name; - if (!KNOWN_TYPES.includes(type)) { - throw new NexusError('UNSUPPORTED', `Key check not implemented for type "${type}"`); - } - const apiKey = - body.apiKey || - ctx.store.getApiKey(name) || - process.env[`${name.toUpperCase()}_API_KEY`]; - if (!apiKey) throw new NexusError('CONFIG', 'No API key available to check'); - const baseUrl = body.baseUrl || row?.baseUrl || 'https://nordrouter.com'; - - const client = new NordRouterClient(apiKey, baseUrl); - const provider = new NordRouterMediaProvider(client, { save: false }); - const result = await provider.checkKey(); - return result; - } catch (err) { - const { statusCode, body } = errorReply(err); - return reply.code(statusCode).send(body); - } - }); - - // --- Usage / stats --- - app.get('/api/stats', async (req) => { - const q = req.query as { sinceDays?: string }; - const sinceDays = q.sinceDays ? Number.parseInt(q.sinceDays, 10) : undefined; - return ctx.journal.stats(Number.isFinite(sinceDays as number) ? sinceDays : undefined); - }); - - // --- Generations (gallery) --- - app.get('/api/generations', async (req) => { - const q = req.query as { limit?: string; offset?: string; capability?: string }; - const limit = Math.min(200, q.limit ? Number.parseInt(q.limit, 10) : 50); - const offset = q.offset ? Number.parseInt(q.offset, 10) : 0; - const items = ctx.journal.list(limit, offset, q.capability); - // Add a media URL for locally-saved files that still exist. - const withUrls = items.map((it) => ({ - ...it, - mediaUrl: it.filePath && fs.existsSync(it.filePath) ? `/media/${path.basename(it.filePath)}` : null, - })); - return { items: withUrls, limit, offset }; - }); - - app.get('/api/generations/:id', async (req, reply) => { - const { id } = req.params as { id: string }; - const rec = ctx.journal.getById(id); - if (!rec) return reply.code(404).send({ error: 'not found', code: 'NOT_FOUND' }); - return { - ...rec, - mediaUrl: rec.filePath && fs.existsSync(rec.filePath) ? `/media/${path.basename(rec.filePath)}` : null, - }; - }); - - app.delete('/api/generations/:id', async (req, reply) => { - try { - const { id } = req.params as { id: string }; - const rec = ctx.journal.getById(id); - if (!rec) return reply.code(404).send({ error: 'not found', code: 'NOT_FOUND' }); - - let fileRemoved = false; - if (rec.filePath) { - const mediaDir = path.resolve(ctx.config.mediaDir); - const resolved = path.resolve(rec.filePath); - // Only delete files that live inside the media dir. - if (resolved.startsWith(mediaDir + path.sep) && fs.existsSync(resolved)) { - try { - fs.unlinkSync(resolved); - fileRemoved = true; - } catch (e) { - log.warn('failed to delete file', e instanceof Error ? e.message : String(e)); - } - } - } - const recordRemoved = ctx.journal.deleteById(id); - return { ok: true, fileRemoved, recordRemoved }; - } catch (err) { - const { statusCode, body } = errorReply(err); - return reply.code(statusCode).send(body); - } - }); - - log.info('routes registered'); -} - -/** Guard: only serve files that live inside the media dir. */ -export function isInsideMediaDir(mediaDir: string, requested: string): boolean { - const resolved = path.resolve(mediaDir, requested); - const rel = path.relative(path.resolve(mediaDir), resolved); - return !rel.startsWith('..') && !path.isAbsolute(rel); -} - -export function ensureStorage(mediaDir: string): MediaStorage { - return new MediaStorage(mediaDir); -} diff --git a/NexusAI/packages/admin/src/server.ts b/NexusAI/packages/admin/src/server.ts deleted file mode 100644 index 09f000c..0000000 --- a/NexusAI/packages/admin/src/server.ts +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node -import * as fs from 'fs'; -import * as path from 'path'; -import Fastify from 'fastify'; -import fastifyStatic from '@fastify/static'; -import { createLogger } from '@nexusai/core'; -import { buildAdminContext } from './context.js'; -import { registerRoutes } from './routes.js'; - -const log = createLogger('admin'); - -async function main(): Promise { - const ctx = buildAdminContext(); - const app = Fastify({ logger: false }); - - registerRoutes(app, ctx); - - // Serve generated media files from the configured media dir. - const mediaDir = path.resolve(ctx.config.mediaDir); - app.get('/media/:file', async (req, reply) => { - const { file } = req.params as { file: string }; - // Prevent path traversal: only a bare filename is allowed. - if (file.includes('/') || file.includes('..') || path.isAbsolute(file)) { - return reply.code(400).send({ error: 'invalid file name' }); - } - const full = path.join(mediaDir, file); - if (!full.startsWith(mediaDir + path.sep) || !fs.existsSync(full)) { - return reply.code(404).send({ error: 'not found' }); - } - const ext = path.extname(full).toLowerCase(); - const CT: Record = { - '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', - '.webp': 'image/webp', '.gif': 'image/gif', - '.mp4': 'video/mp4', '.webm': 'video/webm', - '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4', - '.md': 'text/markdown; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.json': 'application/json', - }; - if (CT[ext]) reply.header('Content-Type', CT[ext]); - const stream = fs.createReadStream(full); - reply.header('Cache-Control', 'private, max-age=60'); - return reply.send(stream); - }); - - // Serve the static frontend (built into ./public next to dist). - const publicDir = path.resolve(__dirname, '../public'); - if (fs.existsSync(publicDir)) { - await app.register(fastifyStatic, { root: publicDir, prefix: '/' }); - } else { - log.warn(`public dir not found at ${publicDir}; UI will not be served`); - } - - const port = Number.parseInt(process.env.NEXUS_ADMIN_PORT || '4123', 10); - const host = process.env.NEXUS_ADMIN_HOST || '127.0.0.1'; - - try { - await app.listen({ port, host }); - log.info(`NexusAI admin running at http://${host}:${port}`); - log.info(`media dir: ${mediaDir}`); - if (!ctx.cipher.enabled) { - log.warn('crypto disabled (NEXUS_MASTER_KEY unset): cannot add/decrypt provider keys'); - } - } catch (err) { - log.error('failed to start admin', err instanceof Error ? err.message : String(err)); - process.exit(1); - } -} - -main(); diff --git a/NexusAI/packages/admin/tsconfig.json b/NexusAI/packages/admin/tsconfig.json deleted file mode 100644 index 3869c10..0000000 --- a/NexusAI/packages/admin/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "node", - "declaration": false, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"] -} diff --git a/NexusAI/packages/core/package.json b/NexusAI/packages/core/package.json deleted file mode 100644 index eb15566..0000000 --- a/NexusAI/packages/core/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@nexusai/core", - "version": "0.1.0", - "description": "NexusAI shared core: providers, media storage, journal, types", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "type": "commonjs", - "scripts": { - "build": "tsc", - "dev": "tsc --watch" - }, - "dependencies": { - "better-sqlite3": "^11.8.0", - "undici": "^6.27.0", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.0", - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } -} diff --git a/NexusAI/packages/core/src/config.ts b/NexusAI/packages/core/src/config.ts deleted file mode 100644 index 7f14143..0000000 --- a/NexusAI/packages/core/src/config.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as os from 'os'; -import * as path from 'path'; - -export interface NordRouterConfig { - apiKey?: string; - baseUrl: string; -} - -export interface SttConfig { - /** Path to python interpreter or "uv" launcher command. */ - pythonCmd: string; - sidecarPath?: string; - model: string; // whisper size: tiny/base/small/medium/large-v3 - device: string; // cpu / cuda / auto - computeType: string; // int8 / float16 / auto -} - -export interface NexusConfig { - defaultProvider: string; - mediaDir: string; - dbPath: string; - /** SQLite path for the encrypted providers store (defaults to same as dbPath). */ - providersDbPath: string; - /** Master secret for encrypting stored API keys (env NEXUS_MASTER_KEY). */ - masterKey?: string; - embedMaxBytes: number; - nordrouter: NordRouterConfig; - stt: SttConfig; -} - -function envInt(name: string, fallback: number): number { - const raw = process.env[name]; - if (!raw) return fallback; - const n = Number.parseInt(raw, 10); - return Number.isFinite(n) ? n : fallback; -} - -export function loadConfig(): NexusConfig { - const mediaDir = - process.env.NEXUS_MEDIA_DIR || path.join(process.cwd(), 'media-output'); - const dbPath = process.env.NEXUS_DB_PATH || path.join(mediaDir, 'nexus.sqlite'); - - return { - defaultProvider: process.env.NEXUS_DEFAULT_PROVIDER || 'nordrouter', - mediaDir, - dbPath, - providersDbPath: process.env.NEXUS_PROVIDERS_DB_PATH || dbPath, - masterKey: process.env.NEXUS_MASTER_KEY, - embedMaxBytes: envInt('NEXUS_EMBED_MAX_BYTES', 10 * 1024 * 1024), - nordrouter: { - apiKey: process.env.NORDROUTER_API_KEY, - baseUrl: (process.env.NORDROUTER_BASE_URL || 'https://nordrouter.com').replace(/\/$/, ''), - }, - stt: { - pythonCmd: process.env.NEXUS_STT_PYTHON || 'uv', - sidecarPath: process.env.NEXUS_STT_SIDECAR, - model: process.env.NEXUS_STT_MODEL || 'small', - device: process.env.NEXUS_STT_DEVICE || 'auto', - computeType: process.env.NEXUS_STT_COMPUTE || 'auto', - }, - }; -} - -export const HOME_DIR = os.homedir(); diff --git a/NexusAI/packages/core/src/db/journal.ts b/NexusAI/packages/core/src/db/journal.ts deleted file mode 100644 index 1993198..0000000 --- a/NexusAI/packages/core/src/db/journal.ts +++ /dev/null @@ -1,158 +0,0 @@ -import Database from 'better-sqlite3'; -import * as fs from 'fs'; -import * as path from 'path'; -import { createLogger } from '../shared/logger.js'; - -const log = createLogger('journal'); - -export interface GenerationRecord { - id: string; - provider: string; - capability: string; - model: string; - inputSummary: string; - status: 'done' | 'failed'; - costUsd: number | null; - filePath: string | null; - resultUrl: string | null; - error: string | null; - durationMs: number | null; - createdAt: string; -} - -export interface UsageStats { - totalCostUsd: number; - totalCount: number; - byProvider: Array<{ provider: string; count: number; costUsd: number }>; - byCapability: Array<{ capability: string; count: number; costUsd: number }>; - byDay: Array<{ day: string; count: number; costUsd: number }>; -} - -/** - * SQLite-backed journal of every generation. Enables usage stats now and a - * shared data source for the future admin package. - */ -export class Journal { - private readonly db: Database.Database; - - constructor(dbPath: string) { - fs.mkdirSync(path.dirname(dbPath), { recursive: true }); - this.db = new Database(dbPath); - this.db.pragma('journal_mode = WAL'); - this.init(); - } - - private init(): void { - this.db.exec(` - CREATE TABLE IF NOT EXISTS generations ( - id TEXT PRIMARY KEY, - provider TEXT NOT NULL, - capability TEXT NOT NULL, - model TEXT NOT NULL, - input_summary TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL, - cost_usd REAL, - file_path TEXT, - result_url TEXT, - error TEXT, - duration_ms INTEGER, - created_at TEXT NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_gen_created ON generations(created_at); - CREATE INDEX IF NOT EXISTS idx_gen_provider ON generations(provider); - CREATE INDEX IF NOT EXISTS idx_gen_capability ON generations(capability); - `); - } - - record(rec: GenerationRecord): void { - try { - this.db - .prepare( - `INSERT INTO generations - (id, provider, capability, model, input_summary, status, cost_usd, file_path, result_url, error, duration_ms, created_at) - VALUES (@id, @provider, @capability, @model, @inputSummary, @status, @costUsd, @filePath, @resultUrl, @error, @durationMs, @createdAt)` - ) - .run(rec); - } catch (err) { - // Journaling must never break a successful generation. - log.warn('failed to record generation', err instanceof Error ? err.message : String(err)); - } - } - - getById(id: string): GenerationRecord | undefined { - const row = this.db - .prepare( - `SELECT id, provider, capability, model, input_summary as inputSummary, status, - cost_usd as costUsd, file_path as filePath, result_url as resultUrl, - error, duration_ms as durationMs, created_at as createdAt - FROM generations WHERE id = ?` - ) - .get(id); - return row as GenerationRecord | undefined; - } - - list(limit = 50, offset = 0, capability?: string): GenerationRecord[] { - const where = capability ? 'WHERE capability = ?' : ''; - const params = capability ? [capability, limit, offset] : [limit, offset]; - const rows = this.db - .prepare( - `SELECT id, provider, capability, model, input_summary as inputSummary, status, - cost_usd as costUsd, file_path as filePath, result_url as resultUrl, - error, duration_ms as durationMs, created_at as createdAt - FROM generations ${where} - ORDER BY created_at DESC LIMIT ? OFFSET ?` - ) - .all(...params); - return rows as GenerationRecord[]; - } - - deleteById(id: string): boolean { - const r = this.db.prepare('DELETE FROM generations WHERE id = ?').run(id); - return r.changes > 0; - } - - stats(sinceDays?: number): UsageStats { - const filter = sinceDays - ? `WHERE created_at >= datetime('now', '-${Math.max(1, Math.floor(sinceDays))} days')` - : ''; - - const total = this.db - .prepare( - `SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as cnt FROM generations ${filter}` - ) - .get() as { cost: number; cnt: number }; - - const byProvider = this.db - .prepare( - `SELECT provider, COUNT(*) as count, COALESCE(SUM(cost_usd),0) as costUsd - FROM generations ${filter} GROUP BY provider ORDER BY costUsd DESC` - ) - .all() as UsageStats['byProvider']; - - const byCapability = this.db - .prepare( - `SELECT capability, COUNT(*) as count, COALESCE(SUM(cost_usd),0) as costUsd - FROM generations ${filter} GROUP BY capability ORDER BY costUsd DESC` - ) - .all() as UsageStats['byCapability']; - - const byDay = this.db - .prepare( - `SELECT substr(created_at,1,10) as day, COUNT(*) as count, COALESCE(SUM(cost_usd),0) as costUsd - FROM generations ${filter} GROUP BY day ORDER BY day DESC LIMIT 30` - ) - .all() as UsageStats['byDay']; - - return { - totalCostUsd: total.cost, - totalCount: total.cnt, - byProvider, - byCapability, - byDay, - }; - } - - close(): void { - this.db.close(); - } -} diff --git a/NexusAI/packages/core/src/db/providers-store.ts b/NexusAI/packages/core/src/db/providers-store.ts deleted file mode 100644 index 200ee39..0000000 --- a/NexusAI/packages/core/src/db/providers-store.ts +++ /dev/null @@ -1,192 +0,0 @@ -import Database from 'better-sqlite3'; -import * as fs from 'fs'; -import * as path from 'path'; -import { NexusError } from '../shared/errors.js'; -import { createLogger } from '../shared/logger.js'; -import { KeyCipher, maskSecret } from '../security/crypto.js'; - -const log = createLogger('providers-store'); - -export interface ProviderRow { - name: string; - type: string; // e.g. "nordrouter" | "routerai" | "openrouter" - baseUrl: string | null; - enabled: boolean; - isDefault: boolean; - createdAt: string; - updatedAt: string; -} - -/** Public view: never exposes the raw key, only a masked hint. */ -export interface ProviderView extends ProviderRow { - hasKey: boolean; - keyHint: string | null; -} - -export interface UpsertProviderInput { - name: string; - type: string; - baseUrl?: string | null; - apiKey?: string; // plaintext; will be encrypted at rest - enabled?: boolean; - isDefault?: boolean; -} - -/** - * SQLite-backed provider registry with encrypted API keys. - * Managed by the admin (Phase 2); read by the MCP server at startup. - */ -export class ProvidersStore { - private readonly db: Database.Database; - - constructor(dbPath: string, private readonly cipher: KeyCipher) { - fs.mkdirSync(path.dirname(dbPath), { recursive: true }); - this.db = new Database(dbPath); - this.db.pragma('journal_mode = WAL'); - this.init(); - } - - private init(): void { - this.db.exec(` - CREATE TABLE IF NOT EXISTS providers ( - name TEXT PRIMARY KEY, - type TEXT NOT NULL, - base_url TEXT, - api_key_enc TEXT, - enabled INTEGER NOT NULL DEFAULT 1, - is_default INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - `); - } - - upsert(input: UpsertProviderInput): void { - const now = new Date().toISOString(); - const encKey = input.apiKey !== undefined ? this.cipher.encrypt(input.apiKey) : undefined; - - const existing = this.db.prepare('SELECT name FROM providers WHERE name = ?').get(input.name); - - if (existing) { - // Update only provided fields; keep existing key if not supplied. - this.db - .prepare( - `UPDATE providers SET - type = @type, - base_url = COALESCE(@baseUrl, base_url), - api_key_enc = COALESCE(@encKey, api_key_enc), - enabled = @enabled, - is_default = @isDefault, - updated_at = @now - WHERE name = @name` - ) - .run({ - name: input.name, - type: input.type, - baseUrl: input.baseUrl ?? null, - encKey: encKey ?? null, - enabled: input.enabled === false ? 0 : 1, - isDefault: input.isDefault ? 1 : 0, - now, - }); - } else { - this.db - .prepare( - `INSERT INTO providers (name, type, base_url, api_key_enc, enabled, is_default, created_at, updated_at) - VALUES (@name, @type, @baseUrl, @encKey, @enabled, @isDefault, @now, @now)` - ) - .run({ - name: input.name, - type: input.type, - baseUrl: input.baseUrl ?? null, - encKey: encKey ?? null, - enabled: input.enabled === false ? 0 : 1, - isDefault: input.isDefault ? 1 : 0, - now, - }); - } - - if (input.isDefault) this.setDefault(input.name); - log.info(`provider upserted: ${input.name}`); - } - - setEnabled(name: string, enabled: boolean): void { - const r = this.db - .prepare('UPDATE providers SET enabled = ?, updated_at = ? WHERE name = ?') - .run(enabled ? 1 : 0, new Date().toISOString(), name); - if (r.changes === 0) throw new NexusError('NOT_FOUND', `Provider not found: ${name}`); - } - - setDefault(name: string): void { - const exists = this.db.prepare('SELECT 1 FROM providers WHERE name = ?').get(name); - if (!exists) throw new NexusError('NOT_FOUND', `Provider not found: ${name}`); - const tx = this.db.transaction(() => { - this.db.prepare('UPDATE providers SET is_default = 0').run(); - this.db - .prepare('UPDATE providers SET is_default = 1, updated_at = ? WHERE name = ?') - .run(new Date().toISOString(), name); - }); - tx(); - } - - remove(name: string): void { - this.db.prepare('DELETE FROM providers WHERE name = ?').run(name); - } - - /** - * Return the decrypted API key for a provider, or undefined. - * Returns undefined (instead of throwing) when no master key is configured, - * so callers can gracefully fall back to env keys. - */ - getApiKey(name: string): string | undefined { - const row = this.db.prepare('SELECT api_key_enc FROM providers WHERE name = ?').get(name) as - | { api_key_enc: string | null } - | undefined; - if (!row?.api_key_enc) return undefined; - if (!this.cipher.enabled) return undefined; - return this.cipher.decrypt(row.api_key_enc); - } - - get(name: string): ProviderRow | undefined { - const row = this.db.prepare('SELECT * FROM providers WHERE name = ?').get(name) as any; - return row ? this.mapRow(row) : undefined; - } - - list(): ProviderRow[] { - const rows = this.db.prepare('SELECT * FROM providers ORDER BY name').all() as any[]; - return rows.map((r) => this.mapRow(r)); - } - - /** Admin-safe listing: masked key hint, never the raw key. */ - listViews(): ProviderView[] { - const rows = this.db.prepare('SELECT * FROM providers ORDER BY name').all() as any[]; - return rows.map((r) => { - const base = this.mapRow(r); - let keyHint: string | null = null; - if (r.api_key_enc) { - try { - keyHint = maskSecret(this.cipher.decrypt(r.api_key_enc)); - } catch { - keyHint = '(undecryptable)'; - } - } - return { ...base, hasKey: Boolean(r.api_key_enc), keyHint }; - }); - } - - private mapRow(r: any): ProviderRow { - return { - name: r.name, - type: r.type, - baseUrl: r.base_url, - enabled: Boolean(r.enabled), - isDefault: Boolean(r.is_default), - createdAt: r.created_at, - updatedAt: r.updated_at, - }; - } - - close(): void { - this.db.close(); - } -} diff --git a/NexusAI/packages/core/src/index.ts b/NexusAI/packages/core/src/index.ts deleted file mode 100644 index 7dd0262..0000000 --- a/NexusAI/packages/core/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * from './config.js'; -export * from './shared/errors.js'; -export * from './shared/logger.js'; -export * from './shared/http.js'; -export * from './shared/polling.js'; -export * from './db/journal.js'; -export * from './db/providers-store.js'; -export * from './security/crypto.js'; -export * from './security/resolver.js'; -export * from './media/storage.js'; -export * from './media/content.js'; -export * from './providers/types.js'; -export * from './providers/registry.js'; -export * from './providers/model-catalog.js'; -export * from './providers/nordrouter/index.js'; -export * from './providers/local-stt/whisper.js'; diff --git a/NexusAI/packages/core/src/media/content.ts b/NexusAI/packages/core/src/media/content.ts deleted file mode 100644 index 11e037d..0000000 --- a/NexusAI/packages/core/src/media/content.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as fs from 'fs'; -import { extToMime } from './storage.js'; -import * as path from 'path'; - -/** - * MCP content block for embedding media directly in a tool result. - * The MCP SDK accepts `image` and `audio` blocks with base64 data + mimeType. - */ -export type EmbeddedContent = - | { type: 'image'; data: string; mimeType: string } - | { type: 'audio'; data: string; mimeType: string }; - -/** - * Build an embedded content block from a local file, respecting a byte cap. - * Returns null when the file is missing, too large, or not embeddable. - */ -export function buildEmbeddedContent( - filePath: string, - mimeType: string | undefined, - maxBytes: number -): EmbeddedContent | null { - try { - const st = fs.statSync(filePath); - if (!st.isFile() || st.size > maxBytes) return null; - - const mime = mimeType || extToMime(path.extname(filePath)); - const buf = fs.readFileSync(filePath); - const data = buf.toString('base64'); - - if (mime.startsWith('image/')) return { type: 'image', data, mimeType: mime }; - if (mime.startsWith('audio/')) return { type: 'audio', data, mimeType: mime }; - return null; // video/other are not embeddable as MCP content - } catch { - return null; - } -} diff --git a/NexusAI/packages/core/src/media/storage.ts b/NexusAI/packages/core/src/media/storage.ts deleted file mode 100644 index 2072d93..0000000 --- a/NexusAI/packages/core/src/media/storage.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { fetch, ProxyAgent } from 'undici'; -import { NexusError } from '../shared/errors.js'; -import { createLogger } from '../shared/logger.js'; - -const log = createLogger('storage'); - -const EXT_BY_MIME: Record = { - 'audio/mpeg': 'mp3', - 'audio/mp3': 'mp3', - 'audio/wav': 'wav', - 'audio/x-wav': 'wav', - 'audio/ogg': 'ogg', - 'image/png': 'png', - 'image/jpeg': 'jpg', - 'image/webp': 'webp', - 'video/mp4': 'mp4', - 'application/octet-stream': 'bin', -}; - -const MIME_BY_EXT: Record = { - mp3: 'audio/mpeg', - wav: 'audio/wav', - ogg: 'audio/ogg', - png: 'image/png', - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - webp: 'image/webp', - mp4: 'video/mp4', -}; - -export function mimeToExt(mime?: string): string { - if (!mime) return 'bin'; - return EXT_BY_MIME[mime.toLowerCase()] || 'bin'; -} - -export function extToMime(ext: string): string { - return MIME_BY_EXT[ext.toLowerCase().replace(/^\./, '')] || 'application/octet-stream'; -} - -function proxyDispatcher(): ProxyAgent | undefined { - const url = - process.env.NEXUS_HTTP_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY; - return url ? new ProxyAgent(url) : undefined; -} - -export interface SaveResult { - filePath: string; - mimeType: string; - bytes: number; -} - -export class MediaStorage { - constructor(private readonly baseDir: string) { - fs.mkdirSync(baseDir, { recursive: true }); - } - - private uniqueName(prefix: string, ext: string): string { - const stamp = new Date().toISOString().replace(/[:.]/g, '-'); - const rand = Math.random().toString(36).slice(2, 8); - return `${prefix}-${stamp}-${rand}.${ext}`; - } - - /** Download a remote URL to a local file. */ - async downloadUrl(url: string, prefix: string): Promise { - const dispatcher = proxyDispatcher(); - const res = await fetch(url, dispatcher ? { dispatcher } : {}); - if (!res.ok) { - throw new NexusError('IO', `Failed to download result (${res.status}) from ${url}`); - } - const mimeType = res.headers.get('content-type')?.split(';')[0]?.trim() || 'application/octet-stream'; - const urlExt = path.extname(new URL(url).pathname).replace(/^\./, ''); - const ext = urlExt || mimeToExt(mimeType); - const buf = Buffer.from(await res.arrayBuffer()); - const filePath = path.join(this.baseDir, this.uniqueName(prefix, ext)); - fs.writeFileSync(filePath, buf); - log.debug('saved file', { filePath, bytes: buf.length }); - return { filePath, mimeType: mimeType === 'application/octet-stream' ? extToMime(ext) : mimeType, bytes: buf.length }; - } - - /** Save a base64 (optionally data-URL) payload to disk. */ - saveBase64(data: string, prefix: string, mimeHint?: string): SaveResult { - let mimeType = mimeHint || 'application/octet-stream'; - let b64 = data; - const dataUrlMatch = /^data:([^;]+);base64,(.*)$/s.exec(data); - if (dataUrlMatch) { - mimeType = dataUrlMatch[1]; - b64 = dataUrlMatch[2]; - } - const buf = Buffer.from(b64, 'base64'); - const ext = mimeToExt(mimeType); - const filePath = path.join(this.baseDir, this.uniqueName(prefix, ext)); - fs.writeFileSync(filePath, buf); - return { filePath, mimeType, bytes: buf.length }; - } - - /** Remove files older than ttlDays. Best-effort. */ - cleanup(ttlDays: number): number { - let removed = 0; - const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1000; - try { - for (const name of fs.readdirSync(this.baseDir)) { - const full = path.join(this.baseDir, name); - try { - const st = fs.statSync(full); - if (st.isFile() && st.mtimeMs < cutoff && !name.endsWith('.sqlite') && !name.includes('nexus.sqlite')) { - fs.unlinkSync(full); - removed += 1; - } - } catch { - /* ignore per-file errors */ - } - } - } catch { - /* ignore */ - } - return removed; - } -} diff --git a/NexusAI/packages/core/src/providers/local-stt/whisper.ts b/NexusAI/packages/core/src/providers/local-stt/whisper.ts deleted file mode 100644 index 7cb14fd..0000000 --- a/NexusAI/packages/core/src/providers/local-stt/whisper.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { spawn } from 'child_process'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { fetch, ProxyAgent } from 'undici'; -import type { SttConfig } from '../../config.js'; -import { NexusError } from '../../shared/errors.js'; -import { createLogger } from '../../shared/logger.js'; -import type { SttInput, SttProvider, SttResult } from '../types.js'; - -const log = createLogger('local-stt'); - -interface SidecarResponse { - ok: boolean; - text?: string; - language?: string; - duration?: number; - segments?: Array<{ start: number; end: number; text: string }>; - error?: string; -} - -/** - * Local speech-to-text via a faster-whisper Python sidecar launched with `uv`. - * The sidecar reads a single JSON request on argv and prints a JSON response. - * Architecture allows swapping in cloud STT adapters later without MCP changes. - */ -export class LocalWhisperProvider implements SttProvider { - readonly name = 'local'; - - constructor(private readonly cfg: SttConfig) {} - - private resolveSidecar(): string { - if (this.cfg.sidecarPath) return this.cfg.sidecarPath; - // packages/core/dist/providers/local-stt/ -> repo/sidecar/stt_server.py - const guess = path.resolve(__dirname, '../../../../../sidecar/stt_server.py'); - return guess; - } - - private async materializeAudio(input: SttInput): Promise<{ file: string; cleanup: boolean }> { - if (input.audioPath) { - if (!fs.existsSync(input.audioPath)) { - throw new NexusError('IO', `Audio file not found: ${input.audioPath}`); - } - return { file: input.audioPath, cleanup: false }; - } - if (input.audioBase64) { - const m = /^data:([^;]+);base64,(.*)$/s.exec(input.audioBase64); - const b64 = m ? m[2] : input.audioBase64; - const tmp = path.join(os.tmpdir(), `nexus-stt-${Date.now()}-${Math.random().toString(36).slice(2)}.audio`); - fs.writeFileSync(tmp, Buffer.from(b64, 'base64')); - return { file: tmp, cleanup: true }; - } - if (input.audioUrl) { - const proxy = process.env.NEXUS_HTTP_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY; - const res = await fetch(input.audioUrl, proxy ? { dispatcher: new ProxyAgent(proxy) } : {}); - if (!res.ok) throw new NexusError('IO', `Failed to download audio (${res.status})`); - const tmp = path.join(os.tmpdir(), `nexus-stt-${Date.now()}-${Math.random().toString(36).slice(2)}.audio`); - fs.writeFileSync(tmp, Buffer.from(await res.arrayBuffer())); - return { file: tmp, cleanup: true }; - } - throw new NexusError('BAD_REQUEST', 'Provide one of: audioPath, audioUrl, audioBase64'); - } - - async transcribe(input: SttInput): Promise { - const { file, cleanup } = await this.materializeAudio(input); - const sidecar = this.resolveSidecar(); - - const request = { - audio: file, - model: input.model || this.cfg.model, - device: this.cfg.device, - compute_type: this.cfg.computeType, - language: input.language, - }; - - // With uv: `uv run --with faster-whisper python sidecar/stt_server.py ` - const usingUv = /(^|\/)uv$/.test(this.cfg.pythonCmd) || this.cfg.pythonCmd === 'uv'; - const sidecarDir = path.dirname(sidecar); - const args = usingUv - ? ['run', sidecar, JSON.stringify(request)] - : [sidecar, JSON.stringify(request)]; - - log.debug('spawning stt sidecar', { cmd: this.cfg.pythonCmd, model: request.model }); - - try { - const response = await this.runSidecar(this.cfg.pythonCmd, args, sidecarDir); - if (!response.ok) { - throw new NexusError('PROVIDER', response.error || 'STT sidecar failed'); - } - return { - provider: this.name, - text: response.text || '', - language: response.language, - durationSec: response.duration, - segments: response.segments, - }; - } finally { - if (cleanup) { - try { - fs.unlinkSync(file); - } catch { - /* ignore */ - } - } - } - } - - private runSidecar(cmd: string, args: string[], cwd: string): Promise { - return new Promise((resolve, reject) => { - // Forward HF mirror / offline settings so the sidecar can locate models - // in restricted networks. - const env = { - ...process.env, - ...(process.env.HF_ENDPOINT ? { HF_ENDPOINT: process.env.HF_ENDPOINT } : {}), - }; - const child = spawn(cmd, args, { cwd, env }); - let stdout = ''; - let stderr = ''; - child.stdout.on('data', (d) => (stdout += d.toString())); - child.stderr.on('data', (d) => (stderr += d.toString())); - child.on('error', (err) => - reject(new NexusError('CONFIG', `Failed to launch STT sidecar (${cmd}): ${err.message}`)) - ); - child.on('close', (code) => { - if (code !== 0 && !stdout.trim()) { - reject(new NexusError('PROVIDER', `STT sidecar exited ${code}: ${stderr.slice(-500)}`)); - return; - } - // The sidecar prints exactly one JSON line on stdout. - const line = stdout.trim().split('\n').filter(Boolean).pop() || ''; - try { - resolve(JSON.parse(line) as SidecarResponse); - } catch { - reject(new NexusError('PROVIDER', `Invalid STT sidecar output: ${stdout.slice(-500)} | stderr: ${stderr.slice(-300)}`)); - } - }); - }); - } -} diff --git a/NexusAI/packages/core/src/providers/model-catalog.ts b/NexusAI/packages/core/src/providers/model-catalog.ts deleted file mode 100644 index 312ff10..0000000 --- a/NexusAI/packages/core/src/providers/model-catalog.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { createLogger } from '../shared/logger.js'; -import type { Capability, MediaProvider, ModelInfo } from './types.js'; - -const log = createLogger('model-catalog'); - -/** Map a NordRouter model `type` to our Capability. */ -export function typeToCapability(type?: string): Capability | undefined { - switch ((type || '').toLowerCase()) { - case 'audio': - return 'tts'; - case 'image': - return 'image'; - case 'video': - return 'video'; - case 'music': - return 'music'; - default: - return undefined; - } -} - -export interface ResolveResult { - /** Exact match found. */ - exact: boolean; - /** The chosen model id (may differ from the requested one when fuzzy-matched). */ - model: string; - /** Human note explaining substitution, if any. */ - note?: string; - /** Alternatives in the same category (for error messages / agent guidance). */ - suggestions?: Array<{ id: string; label?: string; estUsd?: number }>; -} - -export class ModelResolutionError extends Error { - constructor( - message: string, - public readonly capability: Capability, - public readonly suggestions: Array<{ id: string; label?: string; estUsd?: number }> - ) { - super(message); - this.name = 'ModelResolutionError'; - } -} - -/** Normalize an id/label for fuzzy comparison. */ -function norm(s: string): string { - return s.toLowerCase().replace(/^(image|video|audio|music)\//, '').replace(/[^a-z0-9]+/g, ''); -} - -/** Length of the longest common subsequence. */ -function lcs(a: string, b: string): number { - const m = a.length; - const n = b.length; - if (!m || !n) return 0; - let prev = new Array(n + 1).fill(0); - let cur = new Array(n + 1).fill(0); - for (let i = 1; i <= m; i++) { - for (let j = 1; j <= n; j++) { - cur[j] = a[i - 1] === b[j - 1] ? prev[j - 1] + 1 : Math.max(prev[j], cur[j - 1]); - } - [prev, cur] = [cur, prev]; - } - return prev[n]; -} - -/** Cheap similarity in [0,1] combining token overlap and character-level LCS. */ -function similarity(a: string, b: string): number { - const na = norm(a); - const nb = norm(b); - if (!na || !nb) return 0; - if (na === nb) return 1; - if (na.includes(nb) || nb.includes(na)) return 0.85; - - // Token overlap on the original strings. - const ta = new Set(a.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)); - const tb = new Set(b.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)); - let inter = 0; - for (const t of ta) if (tb.has(t)) inter += 1; - const tokenScore = ta.size && tb.size ? inter / Math.max(ta.size, tb.size) : 0; - - // Character-level LCS on normalized strings (handles minor typos / punctuation). - const charScore = lcs(na, nb) / Math.max(na.length, nb.length); - - return Math.max(tokenScore, charScore); -} - -/** - * Caches a provider's model catalog and resolves (possibly misspelled) model ids - * to the nearest valid model within the same capability, preferring the cheapest - * when several are similar. Periodically refreshed in the background. - */ -export class ModelCatalog { - private models: ModelInfo[] = []; - private lastSync = 0; - private syncing?: Promise; - private timer?: NodeJS.Timeout; - - constructor( - private readonly provider: MediaProvider, - private readonly opts: { ttlMs?: number; minSimilarity?: number } = {} - ) {} - - get ttlMs(): number { - return this.opts.ttlMs ?? 10 * 60 * 1000; - } - get minSimilarity(): number { - return this.opts.minSimilarity ?? 0.4; - } - - /** Start periodic background sync. Safe to call once. */ - startAutoSync(intervalMs = 30 * 60 * 1000): void { - if (this.timer) return; - // Fire once soon, then on interval. Unref so it never blocks process exit. - this.timer = setInterval(() => void this.sync().catch(() => {}), intervalMs); - if (typeof this.timer.unref === 'function') this.timer.unref(); - void this.sync().catch(() => {}); - } - - stopAutoSync(): void { - if (this.timer) { - clearInterval(this.timer); - this.timer = undefined; - } - } - - async sync(force = false): Promise { - if (!this.provider.listModels) return; - if (!force && Date.now() - this.lastSync < this.ttlMs && this.models.length) return; - if (this.syncing) return this.syncing; - this.syncing = (async () => { - try { - const models = await this.provider.listModels!(); - this.models = models; - this.lastSync = Date.now(); - log.debug(`synced ${models.length} models for provider "${this.provider.name}"`); - } catch (err) { - log.warn('model sync failed', err instanceof Error ? err.message : String(err)); - } finally { - this.syncing = undefined; - } - })(); - return this.syncing; - } - - /** All known models, syncing lazily if the cache is empty/stale. */ - async all(): Promise { - await this.sync(); - return this.models; - } - - /** Models for a given capability. */ - async byCapability(capability: Capability): Promise { - const all = await this.all(); - return all.filter((m) => typeToCapability(m.type) === capability); - } - - /** - * Resolve a requested model id for a capability. - * - Exact id match → return as-is. - * - Otherwise pick the most similar model above `minSimilarity`; when several - * are equally close, prefer the cheapest. - * - If nothing is close enough, throw ModelResolutionError with suggestions. - */ - async resolve(requested: string, capability: Capability): Promise { - const pool = await this.byCapability(capability); - - // If we couldn't load a catalog at all, don't block the call — trust the id. - if (!pool.length) { - return { exact: true, model: requested, note: 'catalog unavailable; passing id through' }; - } - - const exact = pool.find((m) => m.id === requested); - if (exact) return { exact: true, model: exact.id }; - - const scored = pool - .map((m) => ({ m, score: Math.max(similarity(requested, m.id), similarity(requested, m.label || '')) })) - .sort((a, b) => { - if (b.score !== a.score) return b.score - a.score; - return (a.m.estUsd ?? Infinity) - (b.m.estUsd ?? Infinity); // tie-break: cheapest - }); - - const best = scored[0]; - const suggestions = pool - .slice() - .sort((a, b) => (a.estUsd ?? Infinity) - (b.estUsd ?? Infinity)) - .slice(0, 10) - .map((m) => ({ id: m.id, label: m.label, estUsd: m.estUsd })); - - if (best && best.score >= this.minSimilarity) { - return { - exact: false, - model: best.m.id, - note: `Model "${requested}" not found; using closest match "${best.m.id}"${ - best.m.estUsd != null ? ` (est $${best.m.estUsd})` : '' - }.`, - suggestions, - }; - } - - throw new ModelResolutionError( - `No model matching "${requested}" for capability "${capability}". Ask the user to pick one of the available models.`, - capability, - suggestions - ); - } -} diff --git a/NexusAI/packages/core/src/providers/nordrouter/client.ts b/NexusAI/packages/core/src/providers/nordrouter/client.ts deleted file mode 100644 index 847578e..0000000 --- a/NexusAI/packages/core/src/providers/nordrouter/client.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { HttpClient, RateLimiter } from '../../shared/http.js'; -import { NexusError } from '../../shared/errors.js'; - -/** - * Low-level NordRouter HTTP client. Shared by media, search, upload and models. - * Auth: `Authorization: Bearer sk-...`. Base: https://nordrouter.com - */ -export class NordRouterClient { - readonly http: HttpClient; - readonly baseUrl: string; - - constructor(apiKey: string | undefined, baseUrl: string) { - if (!apiKey) { - throw new NexusError('CONFIG', 'NORDROUTER_API_KEY is not set'); - } - this.baseUrl = baseUrl; - this.http = new HttpClient({ - baseUrl, - defaultHeaders: { Authorization: `Bearer ${apiKey}` }, - timeoutMs: 60_000, - maxRetries: 2, - // NordRouter rate-limits per key; keep it modest and let 429 retries handle bursts. - rateLimiter: new RateLimiter(30, 10_000), - }); - } -} diff --git a/NexusAI/packages/core/src/providers/nordrouter/index.ts b/NexusAI/packages/core/src/providers/nordrouter/index.ts deleted file mode 100644 index 71e817a..0000000 --- a/NexusAI/packages/core/src/providers/nordrouter/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { NordRouterConfig } from '../../config.js'; -import type { MediaStorage } from '../../media/storage.js'; -import { NordRouterClient } from './client.js'; -import { NordRouterMediaProvider } from './media.js'; -import { NordRouterSearchProvider } from './search.js'; - -export { NordRouterClient } from './client.js'; -export { NordRouterMediaProvider } from './media.js'; -export { NordRouterSearchProvider } from './search.js'; - -export interface NordRouterFactoryOptions { - config: NordRouterConfig; - storage: MediaStorage; - save: boolean; -} - -export function createNordRouterProviders(opts: NordRouterFactoryOptions): { - media: NordRouterMediaProvider; - search: NordRouterSearchProvider; -} { - const client = new NordRouterClient(opts.config.apiKey, opts.config.baseUrl); - return { - media: new NordRouterMediaProvider(client, { save: opts.save, storage: opts.storage }), - search: new NordRouterSearchProvider(client), - }; -} diff --git a/NexusAI/packages/core/src/providers/nordrouter/media.ts b/NexusAI/packages/core/src/providers/nordrouter/media.ts deleted file mode 100644 index 0a8316d..0000000 --- a/NexusAI/packages/core/src/providers/nordrouter/media.ts +++ /dev/null @@ -1,193 +0,0 @@ -import * as fs from 'fs'; -import { NexusError } from '../../shared/errors.js'; -import { createLogger } from '../../shared/logger.js'; -import { pollUntilDone } from '../../shared/polling.js'; -import { MediaStorage } from '../../media/storage.js'; -import type { - Capability, - EstimateInput, - MediaProvider, - MediaRequest, - MediaResult, - ModelInfo, - UploadInput, - UploadResult, -} from '../types.js'; -import { NordRouterClient } from './client.js'; - -const log = createLogger('nordrouter:media'); - -interface JobCreateResponse { - id: string; - model: string; - status: string; - poll?: string; - webhook?: string; -} - -interface JobStatusResponse { - id: string; - status: 'processing' | 'done' | 'failed'; - result_url: string | null; - result_url_2?: string | null; - results?: Array<{ index: number; name: string; url: string }>; - cost_usd?: number | null; - error?: string | null; -} - -interface ModelsResponse { - models: Array<{ id: string; type?: string; label?: string; est_usd?: number; fields?: unknown }>; - retention_days?: number; -} - -/** Timeout per capability — images are quick, video/music take longer. */ -const TIMEOUT_MS: Record = { - image: 3 * 60 * 1000, - tts: 3 * 60 * 1000, - music: 8 * 60 * 1000, - video: 15 * 60 * 1000, - stt: 3 * 60 * 1000, - search: 60 * 1000, -}; - -const PREFIX: Record = { - image: 'image', - tts: 'audio', - music: 'music', - video: 'video', - stt: 'stt', - search: 'search', -}; - -export interface NordRouterMediaOptions { - save: boolean; - storage?: MediaStorage; - pollIntervalMs?: number; -} - -export class NordRouterMediaProvider implements MediaProvider { - readonly name = 'nordrouter'; - readonly capabilities: Capability[] = ['tts', 'image', 'video', 'music']; - - private modelsCache?: { at: number; models: ModelInfo[] }; - private readonly modelsTtlMs = 10 * 60 * 1000; - - constructor( - private readonly client: NordRouterClient, - private readonly defaults: NordRouterMediaOptions - ) {} - - async generate(capability: Capability, req: MediaRequest): Promise { - const created = await this.client.http.requestJson({ - method: 'POST', - path: '/media/generate', - body: { model: req.model, input: req.input }, - }); - - if (!created.id) { - throw new NexusError('PROVIDER', 'NordRouter did not return a job id', created); - } - log.debug('job created', { id: created.id, model: req.model }); - - const final = await pollUntilDone({ - intervalMs: this.defaults.pollIntervalMs ?? 3000, - timeoutMs: TIMEOUT_MS[capability], - poll: async () => { - const s = await this.client.http.requestJson({ - method: 'GET', - path: `/media/job/${encodeURIComponent(created.id)}`, - }); - if (s.status === 'failed') return { done: false, failed: true, error: s.error || 'job failed' }; - if (s.status === 'done') return { done: true, value: s }; - return { done: false }; - }, - }); - - const result: MediaResult = { - provider: this.name, - capability, - model: req.model, - status: 'done', - resultUrl: final.result_url ?? undefined, - resultUrl2: final.result_url_2 ?? undefined, - costUsd: final.cost_usd ?? undefined, - raw: final, - }; - - if (this.defaults.save && this.defaults.storage && final.result_url) { - const saved = await this.defaults.storage.downloadUrl(final.result_url, PREFIX[capability]); - result.filePath = saved.filePath; - result.mimeType = saved.mimeType; - } - - return result; - } - - async estimate(req: EstimateInput): Promise<{ usd: number }> { - return this.client.http.requestJson<{ usd: number }>({ - method: 'POST', - path: '/media/estimate', - body: { model: req.model, input: req.input }, - }); - } - - async upload(input: UploadInput): Promise { - if (!fs.existsSync(input.path)) { - throw new NexusError('IO', `Upload file not found: ${input.path}`); - } - const buf = fs.readFileSync(input.path); - const ext = input.path.split('.').pop()?.toLowerCase() || ''; - const mimeByExt: Record = { - png: 'image/png', - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - webp: 'image/webp', - gif: 'image/gif', - mp4: 'video/mp4', - mov: 'video/quicktime', - wav: 'audio/wav', - mp3: 'audio/mpeg', - ogg: 'audio/ogg', - m4a: 'audio/mp4', - }; - const mime = input.mimeType || mimeByExt[ext] || 'application/octet-stream'; - const name = input.name || input.path.split('/').pop() || 'file'; - const dataUrl = `data:${mime};base64,${buf.toString('base64')}`; - const res = await this.client.http.requestJson<{ url?: string }>({ - method: 'POST', - path: '/media/upload', - body: { data: dataUrl, name }, - }); - return { ref: res, url: res.url }; - } - - /** Validate the API key with a zero-cost authenticated call. */ - async checkKey(): Promise<{ ok: boolean; modelsCount?: number; error?: string }> { - try { - const models = await this.listModels(); - return { ok: true, modelsCount: models.length }; - } catch (err) { - return { ok: false, error: err instanceof Error ? err.message : String(err) }; - } - } - - async listModels(): Promise { - const now = Date.now(); - if (this.modelsCache && now - this.modelsCache.at < this.modelsTtlMs) { - return this.modelsCache.models; - } - const res = await this.client.http.requestJson({ - method: 'GET', - path: '/media/models', - }); - const models: ModelInfo[] = (res.models || []).map((m) => ({ - id: m.id, - type: m.type, - label: m.label, - estUsd: m.est_usd, - fields: m.fields, - })); - this.modelsCache = { at: now, models }; - return models; - } -} diff --git a/NexusAI/packages/core/src/providers/nordrouter/search.ts b/NexusAI/packages/core/src/providers/nordrouter/search.ts deleted file mode 100644 index 9604876..0000000 --- a/NexusAI/packages/core/src/providers/nordrouter/search.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { createLogger } from '../../shared/logger.js'; -import type { SearchInput, SearchProvider, SearchResult } from '../types.js'; -import { NordRouterClient } from './client.js'; - -const log = createLogger('nordrouter:search'); - -interface ChatResponse { - choices: Array<{ - message: { - content: string; - annotations?: Array<{ - type: string; - url_citation?: { url: string; title?: string }; - }>; - }; - }>; - usage?: { total_cost?: number }; -} - -const DEFAULT_MODEL = 'perplexity/sonar-pro'; - -/** - * Web search via NordRouter's OpenAI-compatible chat endpoint using - * Perplexity sonar / search-augmented models. Citations arrive in - * `message.annotations[].url_citation`. - */ -export class NordRouterSearchProvider implements SearchProvider { - readonly name = 'nordrouter'; - - constructor(private readonly client: NordRouterClient) {} - - async search(input: SearchInput): Promise { - const model = input.model || DEFAULT_MODEL; - let query = input.query; - if (input.recency) { - query += `\n\n(Focus on information from the last ${input.recency}.)`; - } - - const res = await this.client.http.requestJson({ - method: 'POST', - path: '/v1/chat/completions', - body: { model, messages: [{ role: 'user', content: query }] }, - timeoutMs: 90_000, - }); - - const choice = res.choices?.[0]?.message; - const answer = choice?.content ?? ''; - const seen = new Set(); - const citations: SearchResult['citations'] = []; - let idx = 1; - for (const a of choice?.annotations ?? []) { - const c = a.url_citation; - if (c?.url && !seen.has(c.url)) { - seen.add(c.url); - citations.push({ index: idx++, url: c.url, title: c.title }); - } - } - if (input.maxResults && citations.length > input.maxResults) { - citations.length = input.maxResults; - } - - log.debug('search done', { model, citations: citations.length }); - return { - provider: this.name, - model, - answer, - citations, - costUsd: res.usage?.total_cost, - raw: res, - }; - } -} diff --git a/NexusAI/packages/core/src/providers/registry.ts b/NexusAI/packages/core/src/providers/registry.ts deleted file mode 100644 index dd1af90..0000000 --- a/NexusAI/packages/core/src/providers/registry.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { NexusError } from '../shared/errors.js'; -import type { Capability, MediaProvider, SearchProvider, SttProvider } from './types.js'; - -/** - * Registry maps a provider name + capability to the concrete adapter. - * Adding RouterAI / GPTunnel / OpenRouter / cloud STT means registering a new - * adapter here — no changes needed in the MCP tool layer. - */ -export class ProviderRegistry { - private media = new Map(); - private search = new Map(); - private stt = new Map(); - - registerMedia(provider: MediaProvider): void { - this.media.set(provider.name, provider); - } - registerSearch(provider: SearchProvider): void { - this.search.set(provider.name, provider); - } - registerStt(provider: SttProvider): void { - this.stt.set(provider.name, provider); - } - - getMedia(name: string, capability: Capability): MediaProvider { - const p = this.media.get(name); - if (!p) { - throw new NexusError( - 'UNSUPPORTED', - `Unknown media provider "${name}". Available: ${[...this.media.keys()].join(', ') || 'none'}` - ); - } - if (!p.capabilities.includes(capability)) { - throw new NexusError( - 'UNSUPPORTED', - `Provider "${name}" does not support capability "${capability}"` - ); - } - return p; - } - - getSearch(name: string): SearchProvider { - const p = this.search.get(name); - if (!p) { - throw new NexusError( - 'UNSUPPORTED', - `Unknown search provider "${name}". Available: ${[...this.search.keys()].join(', ') || 'none'}` - ); - } - return p; - } - - getStt(name: string): SttProvider { - const p = this.stt.get(name); - if (!p) { - throw new NexusError( - 'UNSUPPORTED', - `Unknown STT provider "${name}". Available: ${[...this.stt.keys()].join(', ') || 'none'}` - ); - } - return p; - } - - listMediaProviders(): string[] { - return [...this.media.keys()]; - } - listSearchProviders(): string[] { - return [...this.search.keys()]; - } - listSttProviders(): string[] { - return [...this.stt.keys()]; - } -} diff --git a/NexusAI/packages/core/src/providers/types.ts b/NexusAI/packages/core/src/providers/types.ts deleted file mode 100644 index 27654d2..0000000 --- a/NexusAI/packages/core/src/providers/types.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Provider abstraction. Adapters (NordRouter, RouterAI, GPTunnel, OpenRouter, - * local STT) implement one or more of these capabilities. The MCP layer stays - * provider-agnostic and selects an adapter by an explicit `provider` argument. - */ - -export type Capability = 'tts' | 'stt' | 'image' | 'video' | 'music' | 'search'; - -export interface MediaRequest { - model: string; - input: Record; -} - -export interface MediaResult { - provider: string; - capability: Capability; - model: string; - status: 'done'; - /** Local saved file path (when storage enabled). */ - filePath?: string; - mimeType?: string; - /** Remote URL from provider (may expire). */ - resultUrl?: string; - /** Second track for music providers (Suno). */ - resultUrl2?: string; - costUsd?: number; - raw?: unknown; -} - -export interface SttInput { - /** One of: local path, remote URL, or base64 data URL. */ - audioPath?: string; - audioUrl?: string; - audioBase64?: string; - language?: string; - model?: string; -} - -export interface SttResult { - provider: string; - text: string; - language?: string; - durationSec?: number; - segments?: Array<{ start: number; end: number; text: string }>; -} - -export interface SearchInput { - query: string; - model?: string; - recency?: 'day' | 'week' | 'month' | 'year'; - maxResults?: number; -} - -export interface SearchResult { - provider: string; - model: string; - answer: string; - citations: Array<{ index?: number; url: string; title?: string }>; - costUsd?: number; - raw?: unknown; -} - -export interface ModelInfo { - id: string; - type?: string; - label?: string; - estUsd?: number; - fields?: unknown; -} - -export interface EstimateInput { - model: string; - input: Record; -} - -export interface UploadInput { - /** Local path to read and upload. */ - path: string; - name?: string; - mimeType?: string; -} - -export interface UploadResult { - /** Whatever the provider returns to reference the uploaded asset later. */ - ref: unknown; - url?: string; -} - -export interface MediaProvider { - readonly name: string; - readonly capabilities: Capability[]; - generate(capability: Capability, req: MediaRequest): Promise; - estimate?(req: EstimateInput): Promise<{ usd: number }>; - upload?(input: UploadInput): Promise; - listModels?(): Promise; -} - -export interface SearchProvider { - readonly name: string; - search(input: SearchInput): Promise; -} - -export interface SttProvider { - readonly name: string; - transcribe(input: SttInput): Promise; -} diff --git a/NexusAI/packages/core/src/security/crypto.ts b/NexusAI/packages/core/src/security/crypto.ts deleted file mode 100644 index ed26e27..0000000 --- a/NexusAI/packages/core/src/security/crypto.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as crypto from 'crypto'; -import { NexusError } from '../shared/errors.js'; - -/** - * AES-256-GCM encryption for API keys at rest. - * - * The master key is provided via env NEXUS_MASTER_KEY and NEVER stored in the DB. - * Only encrypted key material lives in SQLite, so backups / inspection of the DB - * do not leak provider API keys. Rotating NEXUS_MASTER_KEY invalidates stored - * ciphertext (re-enter keys), which is the intended security trade-off. - * - * Ciphertext format (base64): [ 12-byte IV | 16-byte auth tag | ciphertext ] - */ - -const ALGO = 'aes-256-gcm'; -const IV_LEN = 12; -const TAG_LEN = 16; - -/** - * Derive a 32-byte key from the env master secret. Accepts either a 64-char - * hex string (used directly) or any passphrase (hashed with SHA-256). - */ -export function deriveMasterKey(secret: string | undefined): Buffer | undefined { - if (!secret) return undefined; - const trimmed = secret.trim(); - if (/^[0-9a-fA-F]{64}$/.test(trimmed)) { - return Buffer.from(trimmed, 'hex'); - } - return crypto.createHash('sha256').update(trimmed, 'utf8').digest(); -} - -export class KeyCipher { - private readonly key?: Buffer; - - constructor(masterSecret: string | undefined) { - this.key = deriveMasterKey(masterSecret); - } - - get enabled(): boolean { - return this.key !== undefined; - } - - private requireKey(): Buffer { - if (!this.key) { - throw new NexusError( - 'CONFIG', - 'NEXUS_MASTER_KEY is not set; cannot encrypt/decrypt stored API keys' - ); - } - return this.key; - } - - encrypt(plaintext: string): string { - const key = this.requireKey(); - const iv = crypto.randomBytes(IV_LEN); - const cipher = crypto.createCipheriv(ALGO, key, iv); - const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); - const tag = cipher.getAuthTag(); - return Buffer.concat([iv, tag, enc]).toString('base64'); - } - - decrypt(payload: string): string { - const key = this.requireKey(); - const raw = Buffer.from(payload, 'base64'); - if (raw.length < IV_LEN + TAG_LEN) { - throw new NexusError('CONFIG', 'Malformed encrypted key payload'); - } - const iv = raw.subarray(0, IV_LEN); - const tag = raw.subarray(IV_LEN, IV_LEN + TAG_LEN); - const enc = raw.subarray(IV_LEN + TAG_LEN); - const decipher = crypto.createDecipheriv(ALGO, key, iv); - decipher.setAuthTag(tag); - try { - return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8'); - } catch { - throw new NexusError( - 'CONFIG', - 'Failed to decrypt API key (wrong NEXUS_MASTER_KEY or corrupted data)' - ); - } - } -} - -/** Mask a secret for display: keep last 4 chars. */ -export function maskSecret(secret: string): string { - if (secret.length <= 4) return '****'; - return `${secret.slice(0, 3)}…${secret.slice(-4)}`; -} diff --git a/NexusAI/packages/core/src/security/resolver.ts b/NexusAI/packages/core/src/security/resolver.ts deleted file mode 100644 index 5eec369..0000000 --- a/NexusAI/packages/core/src/security/resolver.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { NexusConfig } from '../config.js'; -import type { ProvidersStore } from '../db/providers-store.js'; -import { createLogger } from '../shared/logger.js'; - -const log = createLogger('key-resolver'); - -export interface ResolvedProvider { - name: string; - type: string; - baseUrl: string; - apiKey?: string; - enabled: boolean; - isDefault: boolean; - /** Where the key came from, for diagnostics. */ - keySource: 'env' | 'db' | 'none'; -} - -/** - * Resolve effective provider settings. - * - * Precedence for keys: ENV wins over DB. This keeps simple/CI setups working - * (just set NORDROUTER_API_KEY) while allowing admin-managed keys in the DB. - * - * Built-in providers (nordrouter) are always considered; DB rows can add more - * or override base URL / enabled / default. - */ -export class KeyResolver { - constructor( - private readonly config: NexusConfig, - private readonly store?: ProvidersStore - ) {} - - private envKeyFor(providerName: string): string | undefined { - // Convention: _API_KEY, e.g. NORDROUTER_API_KEY - const envName = `${providerName.toUpperCase()}_API_KEY`; - return process.env[envName]; - } - - private envBaseUrlFor(providerName: string): string | undefined { - const envName = `${providerName.toUpperCase()}_BASE_URL`; - return process.env[envName]; - } - - /** Resolve one provider by name. */ - resolve(providerName: string, defaultBaseUrl: string): ResolvedProvider { - const dbRow = this.store?.get(providerName); - const envKey = this.envKeyFor(providerName); - // DB key may be unreadable (no master key / rotated key). Degrade gracefully: - // fall back to env instead of crashing startup. - let dbKey: string | undefined; - try { - dbKey = this.store?.getApiKey(providerName); - } catch (err) { - log.warn( - `could not read stored key for "${providerName}" (falling back to env)`, - err instanceof Error ? err.message : String(err) - ); - dbKey = undefined; - } - - const apiKey = envKey ?? dbKey; - const keySource: ResolvedProvider['keySource'] = envKey ? 'env' : dbKey ? 'db' : 'none'; - - const baseUrl = - this.envBaseUrlFor(providerName) || dbRow?.baseUrl || defaultBaseUrl; - - return { - name: providerName, - type: dbRow?.type ?? providerName, - baseUrl, - apiKey, - enabled: dbRow ? dbRow.enabled : true, - isDefault: dbRow?.isDefault ?? false, - keySource, - }; - } - - /** All providers to register: built-ins + any DB-managed ones. */ - resolveAll(builtins: Array<{ name: string; defaultBaseUrl: string }>): ResolvedProvider[] { - const seen = new Set(); - const out: ResolvedProvider[] = []; - - for (const b of builtins) { - out.push(this.resolve(b.name, b.defaultBaseUrl)); - seen.add(b.name); - } - - for (const row of this.store?.list() ?? []) { - if (seen.has(row.name)) continue; - out.push(this.resolve(row.name, row.baseUrl || '')); - seen.add(row.name); - } - - log.debug( - 'resolved providers', - out.map((p) => ({ name: p.name, key: p.keySource, enabled: p.enabled })) - ); - return out; - } - - /** The effective default provider name. */ - resolveDefault(resolved: ResolvedProvider[]): string { - const dbDefault = resolved.find((p) => p.isDefault && p.enabled); - if (dbDefault) return dbDefault.name; - return this.config.defaultProvider; - } -} diff --git a/NexusAI/packages/core/src/shared/errors.ts b/NexusAI/packages/core/src/shared/errors.ts deleted file mode 100644 index fb8c50b..0000000 --- a/NexusAI/packages/core/src/shared/errors.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Unified error type for NexusAI. Every provider adapter should throw NexusError - * so the MCP layer can render a consistent { code, message } shape. - */ -export type NexusErrorCode = - | 'CONFIG' - | 'AUTH' - | 'BAD_REQUEST' - | 'NOT_FOUND' - | 'RATE_LIMIT' - | 'INSUFFICIENT_BALANCE' - | 'PROVIDER' - | 'TIMEOUT' - | 'UNSUPPORTED' - | 'IO' - | 'UNKNOWN'; - -export class NexusError extends Error { - readonly code: NexusErrorCode; - readonly details?: unknown; - - constructor(code: NexusErrorCode, message: string, details?: unknown) { - super(message); - this.name = 'NexusError'; - this.code = code; - this.details = details; - } - - static from(error: unknown, fallbackCode: NexusErrorCode = 'UNKNOWN'): NexusError { - if (error instanceof NexusError) return error; - const message = error instanceof Error ? error.message : String(error); - return new NexusError(fallbackCode, message); - } -} - -/** Map an HTTP status code to a NexusErrorCode. */ -export function codeFromHttpStatus(status: number): NexusErrorCode { - switch (status) { - case 400: - return 'BAD_REQUEST'; - case 401: - case 403: - return 'AUTH'; - case 402: - return 'INSUFFICIENT_BALANCE'; - case 404: - return 'NOT_FOUND'; - case 429: - return 'RATE_LIMIT'; - default: - return status >= 500 ? 'PROVIDER' : 'UNKNOWN'; - } -} diff --git a/NexusAI/packages/core/src/shared/http.ts b/NexusAI/packages/core/src/shared/http.ts deleted file mode 100644 index d51b897..0000000 --- a/NexusAI/packages/core/src/shared/http.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { fetch, ProxyAgent, FormData, type RequestInit, type Response } from 'undici'; -import { NexusError, codeFromHttpStatus } from './errors.js'; -import { sleep } from './polling.js'; - -function getProxyUrl(): string | undefined { - return ( - process.env.NEXUS_HTTP_PROXY || - process.env.HTTPS_PROXY || - process.env.https_proxy || - process.env.HTTP_PROXY || - process.env.http_proxy - ); -} - -function isRetryableError(error: unknown): boolean { - if (error instanceof Error) { - const msg = error.message.toLowerCase(); - return ( - msg.includes('timeout') || - msg.includes('econnreset') || - msg.includes('socket') || - msg.includes('fetch failed') || - msg.includes('und_err') - ); - } - return false; -} - -/** Simple sliding-window rate limiter. */ -export class RateLimiter { - private timestamps: number[] = []; - constructor(private readonly maxRequests: number, private readonly windowMs: number) {} - - async acquire(): Promise { - while (true) { - const now = Date.now(); - this.timestamps = this.timestamps.filter((ts) => now - ts < this.windowMs); - if (this.timestamps.length < this.maxRequests) { - this.timestamps.push(now); - return; - } - const oldest = this.timestamps[0]; - await sleep(Math.max(0, this.windowMs - (now - oldest))); - } - } -} - -export interface HttpClientOptions { - baseUrl: string; - defaultHeaders?: Record; - timeoutMs?: number; - maxRetries?: number; - rateLimiter?: RateLimiter; -} - -export interface RequestOptions { - method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; - path: string; - headers?: Record; - body?: unknown; - timeoutMs?: number; - /** Skip JSON parsing and return raw Response (for binary downloads). */ - raw?: boolean; -} - -export class HttpClient { - private readonly proxyAgent?: ProxyAgent; - - constructor(private readonly opts: HttpClientOptions) { - const proxyUrl = getProxyUrl(); - if (proxyUrl) this.proxyAgent = new ProxyAgent(proxyUrl); - } - - async requestRaw(options: RequestOptions): Promise { - if (this.opts.rateLimiter) await this.opts.rateLimiter.acquire(); - - const url = options.path.startsWith('http') - ? options.path - : `${this.opts.baseUrl}${options.path}`; - const method = options.method ?? 'GET'; - const timeoutMs = options.timeoutMs ?? this.opts.timeoutMs ?? 60_000; - const maxRetries = this.opts.maxRetries ?? 2; - - const headers: Record = { ...this.opts.defaultHeaders, ...options.headers }; - let bodyStr: string | undefined; - let bodyForm: FormData | undefined; - if (options.body !== undefined) { - if (options.body instanceof FormData) { - bodyForm = options.body; - } else { - bodyStr = typeof options.body === 'string' ? options.body : JSON.stringify(options.body); - if (!headers['Content-Type'] && !headers['content-type']) { - headers['Content-Type'] = 'application/json'; - } - } - } - - let lastError: Error | undefined; - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - const init: RequestInit & { dispatcher?: ProxyAgent } = { - method, - headers, - body: bodyForm ?? bodyStr, - signal: AbortSignal.timeout(timeoutMs), - }; - if (this.proxyAgent) init.dispatcher = this.proxyAgent; - - const response = await fetch(url, init); - - if ((response.status === 429 || response.status >= 500) && attempt < maxRetries) { - await sleep(Math.min(1000 * 2 ** attempt, 8000)); - continue; - } - return response; - } catch (error: unknown) { - lastError = error instanceof Error ? error : new Error(String(error)); - if (isRetryableError(lastError) && attempt < maxRetries) { - await sleep(Math.min(1000 * 2 ** attempt, 8000)); - continue; - } - if (lastError.name === 'TimeoutError' || lastError.message.includes('aborted')) { - throw new NexusError('TIMEOUT', `Request to ${url} timed out after ${timeoutMs}ms`); - } - throw NexusError.from(lastError, 'PROVIDER'); - } - } - throw NexusError.from(lastError ?? new Error('Request failed'), 'PROVIDER'); - } - - async requestJson(options: RequestOptions): Promise { - const response = await this.requestRaw(options); - const text = await response.text(); - - if (!response.ok) { - let message = `HTTP ${response.status}`; - try { - const parsed = JSON.parse(text) as { message?: string; error?: { message?: string }; msg?: string }; - message = parsed.message || parsed.error?.message || parsed.msg || message; - } catch { - if (text) message = `${message}: ${text.slice(0, 300)}`; - } - throw new NexusError(codeFromHttpStatus(response.status), message, { status: response.status }); - } - - if (!text) return undefined as T; - try { - return JSON.parse(text) as T; - } catch { - throw new NexusError('PROVIDER', `Invalid JSON response: ${text.slice(0, 300)}`); - } - } -} diff --git a/NexusAI/packages/core/src/shared/logger.ts b/NexusAI/packages/core/src/shared/logger.ts deleted file mode 100644 index 469b379..0000000 --- a/NexusAI/packages/core/src/shared/logger.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * MCP servers communicate over stdio, so ALL logs must go to stderr. - * Never write logs to stdout — it corrupts the JSON-RPC stream. - */ -type Level = 'debug' | 'info' | 'warn' | 'error'; - -const LEVELS: Record = { debug: 10, info: 20, warn: 30, error: 40 }; - -function currentThreshold(): number { - const env = (process.env.NEXUS_LOG_LEVEL || 'info').toLowerCase() as Level; - return LEVELS[env] ?? LEVELS.info; -} - -function emit(level: Level, scope: string, message: string, extra?: unknown): void { - if (LEVELS[level] < currentThreshold()) return; - const ts = new Date().toISOString(); - let line = `[${ts}] ${level.toUpperCase()} (${scope}) ${message}`; - if (extra !== undefined) { - try { - line += ` ${typeof extra === 'string' ? extra : JSON.stringify(extra)}`; - } catch { - line += ' [unserializable extra]'; - } - } - process.stderr.write(line + '\n'); -} - -export interface Logger { - debug(message: string, extra?: unknown): void; - info(message: string, extra?: unknown): void; - warn(message: string, extra?: unknown): void; - error(message: string, extra?: unknown): void; -} - -export function createLogger(scope: string): Logger { - return { - debug: (m, e) => emit('debug', scope, m, e), - info: (m, e) => emit('info', scope, m, e), - warn: (m, e) => emit('warn', scope, m, e), - error: (m, e) => emit('error', scope, m, e), - }; -} diff --git a/NexusAI/packages/core/src/shared/polling.ts b/NexusAI/packages/core/src/shared/polling.ts deleted file mode 100644 index 5238dcc..0000000 --- a/NexusAI/packages/core/src/shared/polling.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NexusError } from './errors.js'; - -export function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -export interface PollOptions { - /** Called every interval. Return { done: true, value } to stop. */ - poll: () => Promise<{ done: boolean; value?: T; failed?: boolean; error?: string }>; - intervalMs?: number; - timeoutMs?: number; - onTick?: (attempt: number, elapsedMs: number) => void; -} - -/** - * Generic polling loop with wall-clock timeout. Used for async media jobs. - */ -export async function pollUntilDone(options: PollOptions): Promise { - const intervalMs = options.intervalMs ?? 3000; - const timeoutMs = options.timeoutMs ?? 10 * 60 * 1000; - const start = Date.now(); - let attempt = 0; - - while (true) { - attempt += 1; - const result = await options.poll(); - options.onTick?.(attempt, Date.now() - start); - - if (result.failed) { - throw new NexusError('PROVIDER', result.error || 'Job failed'); - } - if (result.done) { - return result.value as T; - } - - if (Date.now() - start > timeoutMs) { - throw new NexusError('TIMEOUT', `Polling timed out after ${Math.round(timeoutMs / 1000)}s`); - } - await sleep(intervalMs); - } -} diff --git a/NexusAI/packages/core/tsconfig.json b/NexusAI/packages/core/tsconfig.json deleted file mode 100644 index ad59e93..0000000 --- a/NexusAI/packages/core/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "node", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"] -} diff --git a/NexusAI/packages/mcp-server/package.json b/NexusAI/packages/mcp-server/package.json deleted file mode 100644 index beb62c4..0000000 --- a/NexusAI/packages/mcp-server/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@nexusai/mcp-server", - "version": "0.1.0", - "description": "NexusAI MCP server (stdio): TTS, STT, image/video/music generation, web search", - "main": "dist/index.js", - "type": "commonjs", - "bin": { - "nexusai-mcp": "dist/index.js" - }, - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "start": "node dist/index.js" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.0", - "@nexusai/core": "*", - "zod": "^3.25.0", - "zod-to-json-schema": "^3.25.0" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } -} diff --git a/NexusAI/packages/mcp-server/src/context.ts b/NexusAI/packages/mcp-server/src/context.ts deleted file mode 100644 index 898da82..0000000 --- a/NexusAI/packages/mcp-server/src/context.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - Journal, - KeyCipher, - KeyResolver, - MediaStorage, - ModelCatalog, - ProviderRegistry, - ProvidersStore, - createNordRouterProviders, - loadConfig, - LocalWhisperProvider, - createLogger, - type NexusConfig, -} from '@nexusai/core'; - -const log = createLogger('context'); - -export interface AppContext { - config: NexusConfig; - registry: ProviderRegistry; - storage: MediaStorage; - journal: Journal; - providersStore: ProvidersStore; - /** Model catalogs keyed by provider name (for sync + fuzzy resolution). */ - catalogs: Map; - /** Effective default provider name after resolving DB/env. */ - defaultProvider: string; -} - -/** Built-in providers always considered, even without a DB row. */ -const BUILTINS = [{ name: 'nordrouter', defaultBaseUrl: 'https://nordrouter.com' }]; - -/** - * Wire up providers, storage, journal and the encrypted providers store. - * API keys resolve with ENV > DB precedence (see KeyResolver). - */ -export function buildContext(): AppContext { - const config = loadConfig(); - const storage = new MediaStorage(config.mediaDir); - const journal = new Journal(config.dbPath); - const registry = new ProviderRegistry(); - - const cipher = new KeyCipher(config.masterKey); - const providersStore = new ProvidersStore(config.providersDbPath, cipher); - const resolver = new KeyResolver(config, providersStore); - - if (!cipher.enabled) { - log.warn('NEXUS_MASTER_KEY not set — DB-stored API keys are unavailable; using env keys only'); - } - - const resolved = resolver.resolveAll(BUILTINS); - const defaultProvider = resolver.resolveDefault(resolved); - const catalogs = new Map(); - - for (const p of resolved) { - if (!p.enabled) { - log.info(`provider "${p.name}" disabled — skipped`); - continue; - } - if (p.type === 'nordrouter') { - if (!p.apiKey) { - log.warn(`provider "${p.name}" has no API key (env or DB) — tools will error when used`); - } - try { - const nr = createNordRouterProviders({ - config: { apiKey: p.apiKey, baseUrl: p.baseUrl }, - storage, - save: true, - }); - // Register under the provider's own name so multiple nordrouter-type - // providers with different keys can coexist. - Object.defineProperty(nr.media, 'name', { value: p.name }); - Object.defineProperty(nr.search, 'name', { value: p.name }); - registry.registerMedia(nr.media); - registry.registerSearch(nr.search); - // Model catalog with periodic background sync + fuzzy resolution. - if (p.apiKey) { - const catalog = new ModelCatalog(nr.media); - catalog.startAutoSync(); - catalogs.set(p.name, catalog); - } - log.info(`registered provider "${p.name}" (media + search, key: ${p.keySource})`); - } catch (err) { - log.warn(`failed to register "${p.name}"`, err instanceof Error ? err.message : String(err)); - } - } else { - log.warn(`provider type "${p.type}" (${p.name}) not implemented yet — skipped`); - } - } - - registry.registerStt(new LocalWhisperProvider(config.stt)); - log.info('registered local STT provider'); - log.info(`default provider: ${defaultProvider}`); - - return { config, registry, storage, journal, providersStore, catalogs, defaultProvider }; -} diff --git a/NexusAI/packages/mcp-server/src/handlers.ts b/NexusAI/packages/mcp-server/src/handlers.ts deleted file mode 100644 index c779d40..0000000 --- a/NexusAI/packages/mcp-server/src/handlers.ts +++ /dev/null @@ -1,433 +0,0 @@ -import { randomUUID } from 'crypto'; -import { - ModelResolutionError, - NexusError, - NordRouterClient, - buildEmbeddedContent, - createLogger, - type Capability, - type MediaResult, -} from '@nexusai/core'; -import * as fs from 'fs'; -import * as path from 'path'; -import type { AppContext } from './context.js'; -import { - ImageSchema, - MediaEstimateSchema, - MediaModelsSchema, - MediaUploadSchema, - MusicSchema, - SttSchema, - TtsSchema, - UsageStatsSchema, - VideoSchema, - VisionSchema, - WebSearchSchema, -} from './schemas.js'; - -const log = createLogger('handlers'); - -type McpContent = - | { type: 'text'; text: string } - | { type: 'image'; data: string; mimeType: string } - | { type: 'audio'; data: string; mimeType: string }; - -export interface McpResult { - content: McpContent[]; - isError?: boolean; -} - -function jsonBlock(obj: unknown): McpContent { - return { type: 'text', text: JSON.stringify(obj, null, 2) }; -} - -function summarize(input: Record): string { - const s = JSON.stringify(input); - return s.length > 200 ? s.slice(0, 197) + '...' : s; -} - -export class Handlers { - constructor(private readonly ctx: AppContext) {} - - private providerName(explicit?: string): string { - return explicit || this.ctx.defaultProvider; - } - - async handle(name: string, args: Record): Promise { - switch (name) { - case 'nexus_tts': - return this.media('tts', TtsSchema.parse(args)); - case 'nexus_image': - return this.media('image', ImageSchema.parse(args)); - case 'nexus_video': - return this.media('video', VideoSchema.parse(args)); - case 'nexus_music': - return this.media('music', MusicSchema.parse(args)); - case 'nexus_stt': - return this.stt(SttSchema.parse(args)); - case 'nexus_web_search': - return this.search(WebSearchSchema.parse(args)); - case 'nexus_media_models': - return this.models(MediaModelsSchema.parse(args)); - case 'nexus_media_estimate': - return this.estimate(MediaEstimateSchema.parse(args)); - case 'nexus_media_upload': - return this.upload(MediaUploadSchema.parse(args)); - case 'nexus_usage_stats': - return this.usage(UsageStatsSchema.parse(args)); - case 'nexus_vision': - return this.vision(VisionSchema.parse(args)); - default: - throw new NexusError('NOT_FOUND', `Unknown tool: ${name}`); - } - } - - private async media( - capability: Capability, - args: { provider?: string; model: string; input: Record; output?: { save?: boolean; embed?: boolean } } - ): Promise { - const providerName = this.providerName(args.provider); - const provider = this.ctx.registry.getMedia(providerName, capability); - const started = Date.now(); - const id = randomUUID(); - - // Resolve the model against the synced catalog: exact, nearest-cheapest, or - // a helpful error listing available models for the agent to relay. - let model = args.model; - let resolutionNote: string | undefined; - const catalog = this.ctx.catalogs.get(providerName); - if (catalog) { - try { - const r = await catalog.resolve(args.model, capability); - model = r.model; - resolutionNote = r.note; - } catch (err) { - if (err instanceof ModelResolutionError) { - throw new NexusError('BAD_REQUEST', err.message, { - capability: err.capability, - requested: args.model, - available: err.suggestions, - hint: 'Ask the user to choose one of the available models (sorted cheapest first).', - }); - } - throw err; - } - } - - try { - const result = await provider.generate(capability, { model, input: args.input }); - this.ctx.journal.record({ - id, - provider: providerName, - capability, - model, - inputSummary: summarize(args.input), - status: 'done', - costUsd: result.costUsd ?? null, - filePath: result.filePath ?? null, - resultUrl: result.resultUrl ?? null, - error: null, - durationMs: Date.now() - started, - createdAt: new Date().toISOString(), - }); - return this.mediaResult(result, args.output?.embed ?? false, resolutionNote); - } catch (err) { - const e = NexusError.from(err); - this.ctx.journal.record({ - id, - provider: providerName, - capability, - model, - inputSummary: summarize(args.input), - status: 'failed', - costUsd: null, - filePath: null, - resultUrl: null, - error: `${e.code}: ${e.message}`, - durationMs: Date.now() - started, - createdAt: new Date().toISOString(), - }); - throw e; - } - } - - private mediaResult(result: MediaResult, embed: boolean, note?: string): McpResult { - const content: McpContent[] = [ - jsonBlock({ - provider: result.provider, - capability: result.capability, - model: result.model, - status: result.status, - file_path: result.filePath, - mime_type: result.mimeType, - result_url: result.resultUrl, - result_url_2: result.resultUrl2, - cost_usd: result.costUsd, - ...(note ? { note } : {}), - }), - ]; - - if (embed && result.filePath) { - const block = buildEmbeddedContent(result.filePath, result.mimeType, this.ctx.config.embedMaxBytes); - if (block) content.push(block); - else content.push({ type: 'text', text: '(embed skipped: file too large or not embeddable)' }); - } - return { content }; - } - - private async stt(args: { - provider?: string; - input: { audioPath?: string; audioUrl?: string; audioBase64?: string; language?: string; model?: string }; - }): Promise { - const providerName = args.provider || 'local'; - const provider = this.ctx.registry.getStt(providerName); - const started = Date.now(); - const id = randomUUID(); - try { - const res = await provider.transcribe(args.input); - // Persist the transcript as a Markdown file so it shows up in the gallery - // and can be read in the modal viewer. - const transcriptPath = this.writeTranscript(id, res, args.input); - this.ctx.journal.record({ - id, - provider: providerName, - capability: 'stt', - model: args.input.model || 'default', - inputSummary: summarize({ ...args.input, audioBase64: args.input.audioBase64 ? '[base64]' : undefined }), - status: 'done', - costUsd: 0, - filePath: transcriptPath, - resultUrl: null, - error: null, - durationMs: Date.now() - started, - createdAt: new Date().toISOString(), - }); - return { - content: [ - jsonBlock({ - provider: res.provider, - language: res.language, - duration_sec: res.durationSec, - text: res.text, - segments: res.segments, - }), - ], - }; - } catch (err) { - const e = NexusError.from(err); - this.ctx.journal.record({ - id, - provider: providerName, - capability: 'stt', - model: args.input.model || 'default', - inputSummary: 'stt', - status: 'failed', - costUsd: null, - filePath: null, - resultUrl: null, - error: `${e.code}: ${e.message}`, - durationMs: Date.now() - started, - createdAt: new Date().toISOString(), - }); - throw e; - } - } - - /** Write the STT transcript to a Markdown file in the media dir. Returns the path or null. */ - private writeTranscript( - id: string, - res: { text: string; language?: string; durationSec?: number; segments?: Array<{ start: number; end: number; text: string }> }, - input: { audioPath?: string; audioUrl?: string; model?: string } - ): string | null { - try { - const source = input.audioPath || input.audioUrl || '(inline audio)'; - const lines: string[] = [ - '# Транскрипция', - '', - `- **Источник:** ${source}`, - `- **Язык:** ${res.language ?? 'auto'}`, - `- **Длительность:** ${res.durationSec != null ? res.durationSec + ' сек' : '—'}`, - `- **Модель:** ${input.model || 'default'}`, - `- **Создано:** ${new Date().toISOString()}`, - '', - '## Текст', - '', - res.text || '_(пусто)_', - ]; - if (res.segments?.length) { - lines.push('', '## Сегменты', ''); - for (const s of res.segments) { - lines.push(`- \`[${s.start.toFixed(2)}–${s.end.toFixed(2)}]\` ${s.text.trim()}`); - } - } - const stamp = new Date().toISOString().replace(/[:.]/g, '-'); - const filePath = path.join(this.ctx.config.mediaDir, `transcript-${stamp}-${id.slice(0, 8)}.md`); - fs.mkdirSync(this.ctx.config.mediaDir, { recursive: true }); - fs.writeFileSync(filePath, lines.join('\n'), 'utf8'); - return filePath; - } catch (err) { - log.warn('failed to write transcript', err instanceof Error ? err.message : String(err)); - return null; - } - } - - private async search(args: { - provider?: string; - query: string; - model?: string; - recency?: 'day' | 'week' | 'month' | 'year'; - maxResults?: number; - }): Promise { - const providerName = this.providerName(args.provider); - const provider = this.ctx.registry.getSearch(providerName); - const started = Date.now(); - const id = randomUUID(); - const res = await provider.search({ - query: args.query, - model: args.model, - recency: args.recency, - maxResults: args.maxResults, - }); - this.ctx.journal.record({ - id, - provider: providerName, - capability: 'search', - model: res.model, - inputSummary: summarize({ query: args.query }), - status: 'done', - costUsd: res.costUsd ?? null, - filePath: null, - resultUrl: null, - error: null, - durationMs: Date.now() - started, - createdAt: new Date().toISOString(), - }); - return { - content: [jsonBlock({ provider: res.provider, model: res.model, answer: res.answer, citations: res.citations })], - }; - } - - private async models(args: { provider?: string; type?: string; search?: string }): Promise { - const providerName = this.providerName(args.provider); - // any capability that this provider supports is fine to fetch the catalog - const provider = this.ctx.registry.getMedia(providerName, 'image'); - if (!provider.listModels) throw new NexusError('UNSUPPORTED', `Provider ${providerName} cannot list models`); - let models = await provider.listModels(); - if (args.type) models = models.filter((m) => m.type === args.type); - if (args.search) { - const q = args.search.toLowerCase(); - models = models.filter((m) => m.id.toLowerCase().includes(q) || (m.label || '').toLowerCase().includes(q)); - } - return { content: [jsonBlock({ count: models.length, models })] }; - } - - private async estimate(args: { provider?: string; model: string; input: Record }): Promise { - const providerName = this.providerName(args.provider); - const provider = this.ctx.registry.getMedia(providerName, 'image'); - if (!provider.estimate) throw new NexusError('UNSUPPORTED', `Provider ${providerName} cannot estimate`); - const est = await provider.estimate({ model: args.model, input: args.input }); - return { content: [jsonBlock({ model: args.model, usd: est.usd })] }; - } - - private async upload(args: { provider?: string; path: string; name?: string; mimeType?: string }): Promise { - const providerName = this.providerName(args.provider); - const provider = this.ctx.registry.getMedia(providerName, 'image'); - if (!provider.upload) throw new NexusError('UNSUPPORTED', `Provider ${providerName} cannot upload`); - const res = await provider.upload({ path: args.path, name: args.name, mimeType: args.mimeType }); - return { content: [jsonBlock({ url: res.url, ref: res.ref })] }; - } - - private async usage(args: { sinceDays?: number; list?: boolean; limit: number }): Promise { - const stats = this.ctx.journal.stats(args.sinceDays); - const payload: Record = { stats }; - if (args.list) payload.recent = this.ctx.journal.list(args.limit); - log.debug('usage stats requested'); - return { content: [jsonBlock(payload)] }; - } - - private async vision(args: { - provider?: string; - image: string; - prompt: string; - model?: string; - }): Promise { - const providerName = this.providerName(args.provider); - const row = this.ctx.providersStore.get(providerName); - const apiKey = this.ctx.providersStore.getApiKey(providerName); - const baseUrl = row?.baseUrl || process.env[`${providerName.toUpperCase()}_BASE_URL`] || 'https://nordrouter.com'; - if (!apiKey) { - throw new NexusError('CONFIG', `No API key for provider "${providerName}"`); - } - - let imageUrl = args.image; - if (!args.image.startsWith('http') && !args.image.startsWith('data:')) { - if (!fs.existsSync(args.image)) { - throw new NexusError('IO', `Image file not found: ${args.image}`); - } - const buf = fs.readFileSync(args.image); - const ext = args.image.split('.').pop()?.toLowerCase() || 'png'; - const mime = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'webp' ? 'image/webp' : 'image/png'; - imageUrl = `data:${mime};base64,${buf.toString('base64')}`; - } - - const client = new NordRouterClient(apiKey, baseUrl); - const started = Date.now(); - const id = randomUUID(); - const model = args.model || 'google/gemini-3.1-flash-lite'; - - try { - const res = await client.http.requestJson<{ - choices?: Array<{ message?: { content?: string } }>; - usage?: { total_cost?: number }; - }>({ - method: 'POST', - path: '/v1/chat/completions', - body: { - model, - messages: [ - { - role: 'user', - content: [ - { type: 'text', text: args.prompt }, - { type: 'image_url', image_url: { url: imageUrl } }, - ], - }, - ], - }, - }); - const text = res.choices?.[0]?.message?.content?.trim() || ''; - this.ctx.journal.record({ - id, - provider: providerName, - capability: 'vision', - model, - inputSummary: summarize({ prompt: args.prompt, image: args.image }), - status: 'done', - costUsd: res.usage?.total_cost ?? null, - filePath: null, - resultUrl: null, - error: null, - durationMs: Date.now() - started, - createdAt: new Date().toISOString(), - }); - return { content: [jsonBlock({ provider: providerName, model, description: text })] }; - } catch (err) { - const e = NexusError.from(err); - this.ctx.journal.record({ - id, - provider: providerName, - capability: 'vision', - model, - inputSummary: summarize({ prompt: args.prompt, image: args.image }), - status: 'failed', - costUsd: null, - filePath: null, - resultUrl: null, - error: `${e.code}: ${e.message}`, - durationMs: Date.now() - started, - createdAt: new Date().toISOString(), - }); - throw e; - } - } -} diff --git a/NexusAI/packages/mcp-server/src/index.ts b/NexusAI/packages/mcp-server/src/index.ts deleted file mode 100644 index 5c06bcd..0000000 --- a/NexusAI/packages/mcp-server/src/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env node -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, - type Tool, -} from '@modelcontextprotocol/sdk/types.js'; -import { NexusError, createLogger } from '@nexusai/core'; -import { buildContext } from './context.js'; -import { Handlers } from './handlers.js'; -import { TOOLS } from './tools.js'; - -const log = createLogger('server'); - -function main(): void { - const ctx = buildContext(); - const handlers = new Handlers(ctx); - - const server = new Server( - { name: 'nexusai-mcp-server', version: '0.1.0' }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: TOOLS.map((t) => ({ - name: t.name, - description: t.description, - inputSchema: t.inputSchema, - })) as Tool[], - })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - try { - return (await handlers.handle(name, args ?? {})) as never; - } catch (error) { - const e = NexusError.from(error); - log.error(`tool ${name} failed`, `${e.code}: ${e.message}`); - return { - content: [{ type: 'text', text: `Error [${e.code}]: ${e.message}` }], - isError: true, - } as never; - } - }); - - const transport = new StdioServerTransport(); - server.connect(transport).catch((error: unknown) => { - log.error('failed to start MCP server', error instanceof Error ? error.message : String(error)); - process.exit(1); - }); - - log.info(`NexusAI MCP server started with ${TOOLS.length} tools`); -} - -main(); diff --git a/NexusAI/packages/mcp-server/src/schemas.ts b/NexusAI/packages/mcp-server/src/schemas.ts deleted file mode 100644 index 25c24f3..0000000 --- a/NexusAI/packages/mcp-server/src/schemas.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { z } from 'zod'; - -/** Common output options for media tools. */ -const OutputOptions = z - .object({ - save: z.boolean().default(true).describe('Save the result to a local file.'), - embed: z - .boolean() - .default(false) - .describe('Also embed image/audio directly in the MCP result (size-limited).'), - }) - .partial() - .default({}); - -const provider = z - .string() - .optional() - .describe('Provider name. Defaults to the configured default provider (e.g. "nordrouter").'); - -export const TtsSchema = z.object({ - provider, - model: z - .string() - .default('audio/elevenlabs-tts') - .describe('TTS model id, e.g. audio/elevenlabs-tts, audio/elevenlabs-multilingual, audio/gemini-3.1-flash-tts.'), - input: z - .object({ - text: z.string().describe('Text to synthesize.'), - voice: z.string().optional().describe('Voice name (see nexus_media_models for options).'), - voice_id: z.string().optional(), - speed: z.union([z.string(), z.number()]).optional().describe('Speed, e.g. "0.8" | "1.0" | "1.2".'), - }) - .passthrough() - .describe('Provider input. Extra model-specific fields are passed through.'), - output: OutputOptions, -}); - -export const ImageSchema = z.object({ - provider, - model: z - .string() - .default('image/nano-banana-2') - .describe('Image model id, e.g. image/nano-banana-2, image/flux2-pro, image/gpt-image-2, image/z-image. Use *-edit models for editing.'), - input: z - .object({ - prompt: z.string().describe('Image description. For edits also pass "image".'), - image: z.string().optional().describe('Source image URL for edit/i2i models.'), - aspect_ratio: z.string().optional(), - resolution: z.string().optional().describe('1K | 2K | 4K (model dependent).'), - }) - .passthrough(), - output: OutputOptions, -}); - -export const VideoSchema = z.object({ - provider, - model: z - .string() - .default('video/kling-2.6-t2v') - .describe('Video model id, e.g. video/kling-2.6-t2v, video/veo-3.1-fast, video/seedance-2. Use i2v models with an "image".'), - input: z - .object({ - prompt: z.string().describe('Video description.'), - image: z.string().optional().describe('First-frame image URL for i2v models.'), - image2: z.string().optional().describe('Last-frame image URL (transition models).'), - duration: z.union([z.string(), z.number()]).optional(), - resolution: z.string().optional(), - aspect_ratio: z.string().optional(), - sound: z.boolean().optional(), - }) - .passthrough(), - output: OutputOptions, -}); - -export const MusicSchema = z.object({ - provider, - model: z - .string() - .default('music/suno-v5') - .describe('Music model id, e.g. music/suno-v5, music/suno-cover, music/suno-sounds.'), - input: z - .object({ - title: z.string().optional(), - style: z.string().optional(), - prompt: z.string().optional().describe('Lyrics/prompt.'), - instrumental: z.boolean().optional(), - version: z.string().optional().describe('V4 | V4_5 | V4_5PLUS | V4_5ALL | V5 | V5_5.'), - }) - .passthrough(), - output: OutputOptions, -}); - -export const SttSchema = z.object({ - provider: z.string().optional().describe('STT provider. Defaults to "local" (faster-whisper).'), - input: z.object({ - audioPath: z.string().optional().describe('Local audio file path.'), - audioUrl: z.string().optional().describe('Remote audio URL.'), - audioBase64: z.string().optional().describe('Base64 or data-URL audio.'), - language: z.string().optional().describe('Language hint, e.g. "ru", "en". Auto-detect if omitted.'), - model: z.string().optional().describe('Whisper size: tiny|base|small|medium|large-v3.'), - }), -}); - -export const WebSearchSchema = z.object({ - provider, - query: z.string().describe('Search query / question.'), - model: z - .string() - .optional() - .describe('Search model, e.g. perplexity/sonar, perplexity/sonar-pro, or search/.'), - recency: z.enum(['day', 'week', 'month', 'year']).optional(), - maxResults: z.number().int().positive().max(50).optional(), -}); - -export const VisionSchema = z.object({ - provider, - image: z.string().describe('Image URL, local file path, or base64/data-URL.'), - prompt: z - .string() - .default('Describe the scene in this image in detail, as if writing a prompt for video generation.') - .describe('What to ask the vision model about the image.'), - model: z - .string() - .optional() - .describe('Vision-capable chat model, e.g. google/gemini-3.1-flash-lite, openai/gpt-5.4-mini.'), -}); - -export const MediaModelsSchema = z.object({ - provider, - type: z.enum(['image', 'video', 'music', 'audio', 'upscale']).optional().describe('Filter by model type.'), - search: z.string().optional().describe('Substring filter on model id/label.'), -}); - -export const MediaEstimateSchema = z.object({ - provider, - model: z.string().describe('Model id to estimate.'), - input: z.record(z.unknown()).default({}).describe('Same input you would pass to generate.'), -}); - -export const MediaUploadSchema = z.object({ - provider, - path: z.string().describe('Local file path to upload.'), - name: z.string().optional(), - mimeType: z.string().optional(), -}); - -export const UsageStatsSchema = z.object({ - sinceDays: z.number().int().positive().optional().describe('Limit stats to the last N days.'), - list: z.boolean().default(false).describe('Also return recent generations.'), - limit: z.number().int().positive().max(200).default(20), -}); diff --git a/NexusAI/packages/mcp-server/src/tools.ts b/NexusAI/packages/mcp-server/src/tools.ts deleted file mode 100644 index e0e87ad..0000000 --- a/NexusAI/packages/mcp-server/src/tools.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - ImageSchema, - MediaEstimateSchema, - MediaModelsSchema, - MediaUploadSchema, - MusicSchema, - SttSchema, - TtsSchema, - UsageStatsSchema, - VideoSchema, - VisionSchema, - WebSearchSchema, -} from './schemas.js'; - -export interface ToolDef { - name: string; - description: string; - inputSchema: object; -} - -function make(name: string, description: string, schema: unknown): ToolDef { - const json = zodToJsonSchema(schema as any, { name, $refStrategy: 'none' }) as any; - const def = json.definitions?.[name] ?? json; - return { name, description, inputSchema: def }; -} - -export const TOOLS: ToolDef[] = [ - make( - 'nexus_tts', - 'Text-to-speech. Generates spoken audio from text via a provider. Returns a saved audio file path and optional embedded audio.', - TtsSchema - ), - make( - 'nexus_image', - 'Generate or edit images from a text prompt (and optional source image URL). Returns a saved image file and optional embedded image.', - ImageSchema - ), - make( - 'nexus_video', - 'Generate video from text or an image (t2v / i2v). Async, may take minutes. Returns a saved MP4 file path.', - VideoSchema - ), - make( - 'nexus_music', - 'Generate music from a style/prompt. Returns saved audio file(s); some models produce two tracks.', - MusicSchema - ), - make( - 'nexus_stt', - 'Speech-to-text. Transcribe local/remote/base64 audio using a local whisper model. Returns text, language and segments.', - SttSchema - ), - make( - 'nexus_web_search', - 'Web search that returns an answer with cited source URLs (via search-augmented models).', - WebSearchSchema - ), - make( - 'nexus_media_models', - 'List available media models (id, type, label, estimated price) for a provider. Use to discover valid model ids and parameters.', - MediaModelsSchema - ), - make( - 'nexus_media_estimate', - 'Estimate the USD cost of a media generation before running it.', - MediaEstimateSchema - ), - make( - 'nexus_media_upload', - 'Upload a local media file to the provider and get a reference URL for use as input (image/audio/video).', - MediaUploadSchema - ), - make( - 'nexus_usage_stats', - 'Show usage and spend recorded in the local journal (totals by provider, capability, day) and optionally recent generations.', - UsageStatsSchema - ), - make( - 'nexus_vision', - 'Describe an image using a vision-capable chat model. Returns a text description useful for prompts.', - VisionSchema - ), -]; diff --git a/NexusAI/packages/mcp-server/tsconfig.json b/NexusAI/packages/mcp-server/tsconfig.json deleted file mode 100644 index 3869c10..0000000 --- a/NexusAI/packages/mcp-server/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "node", - "declaration": false, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"] -} diff --git a/NexusAI/sidecar/stt_server.py b/NexusAI/sidecar/stt_server.py deleted file mode 100644 index 2775d0d..0000000 --- a/NexusAI/sidecar/stt_server.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -# /// script -# requires-python = ">=3.9" -# dependencies = [ -# "faster-whisper>=1.0.0", -# ] -# /// -""" -NexusAI local STT sidecar (faster-whisper). - -Invoked by the TypeScript MCP server as: - uv run sidecar/stt_server.py '' - -Request JSON: - { "audio": "/path/to/file", "model": "small", "device": "auto", - "compute_type": "auto", "language": "ru" } - -Prints exactly ONE JSON line on stdout: - { "ok": true, "text": "...", "language": "ru", "duration": 12.3, - "segments": [ { "start": 0.0, "end": 2.1, "text": "..." } ] } -On error: - { "ok": false, "error": "message" } - -All diagnostics go to stderr so stdout stays a clean single JSON line. -faster-whisper decodes audio via bundled ffmpeg/pyav, so no manual conversion -is required for common formats (mp3/wav/ogg/m4a/webm...). -""" -import json -import sys - - -def log(msg: str) -> None: - print(msg, file=sys.stderr, flush=True) - - -def resolve_device(device: str, compute_type: str): - if device and device != "auto": - ct = compute_type if compute_type and compute_type != "auto" else ( - "float16" if device == "cuda" else "int8" - ) - return device, ct - # auto-detect: try cuda, fall back to cpu - try: - import ctranslate2 # noqa: F401 - cuda_count = 0 - try: - cuda_count = ctranslate2.get_cuda_device_count() - except Exception: - cuda_count = 0 - if cuda_count > 0: - return "cuda", (compute_type if compute_type != "auto" else "float16") - except Exception: - pass - return "cpu", (compute_type if compute_type != "auto" else "int8") - - -def main() -> int: - if len(sys.argv) < 2: - print(json.dumps({"ok": False, "error": "missing request argument"})) - return 1 - - try: - req = json.loads(sys.argv[1]) - except Exception as exc: - print(json.dumps({"ok": False, "error": f"invalid request json: {exc}"})) - return 1 - - audio = req.get("audio") - if not audio: - print(json.dumps({"ok": False, "error": "no audio path provided"})) - return 1 - - model_size = req.get("model") or "small" - device, compute_type = resolve_device(req.get("device", "auto"), req.get("compute_type", "auto")) - language = req.get("language") or None - - try: - from faster_whisper import WhisperModel - except Exception as exc: - print(json.dumps({"ok": False, "error": f"faster-whisper not available: {exc}"})) - return 1 - - # Allow pointing at a local model dir (offline) or a mirror via env. - # NEXUS_STT_MODEL may be a whisper size name OR an absolute path to a - # pre-downloaded CTranslate2 model directory. - import os - model_ref = model_size - local_only = os.environ.get("NEXUS_STT_LOCAL_ONLY", "").lower() in ("1", "true", "yes") - download_root = os.environ.get("NEXUS_STT_DOWNLOAD_ROOT") or None - - try: - log(f"loading model={model_ref} device={device} compute={compute_type} local_only={local_only}") - model = WhisperModel( - model_ref, - device=device, - compute_type=compute_type, - download_root=download_root, - local_files_only=local_only, - ) - segments_iter, info = model.transcribe(audio, language=language, vad_filter=True) - - segments = [] - parts = [] - for seg in segments_iter: - parts.append(seg.text) - segments.append({"start": round(seg.start, 3), "end": round(seg.end, 3), "text": seg.text.strip()}) - - result = { - "ok": True, - "text": "".join(parts).strip(), - "language": info.language, - "duration": round(getattr(info, "duration", 0.0) or 0.0, 3), - "segments": segments, - } - print(json.dumps(result, ensure_ascii=False)) - return 0 - except Exception as exc: - hint = "" - msg = str(exc) - if "connect" in msg.lower() or "internet" in msg.lower() or "Hub" in msg: - hint = ( - " | Model download failed. Options: set HF_ENDPOINT to a reachable mirror, " - "pre-download the model and set NEXUS_STT_MODEL to its local path with " - "NEXUS_STT_LOCAL_ONLY=1, or set NEXUS_STT_DOWNLOAD_ROOT to a cache dir." - ) - print(json.dumps({"ok": False, "error": f"transcription failed: {exc}{hint}"}, ensure_ascii=False)) - return 1 - - -if __name__ == "__main__": - sys.exit(main())