diff --git a/frontend/favicon.ico b/frontend/favicon.ico old mode 100644 new mode 100755 index aa4cfff..c8150a9 Binary files a/frontend/favicon.ico and b/frontend/favicon.ico differ diff --git a/frontend/src/components/ui/DeleteDialog.tsx b/frontend/src/components/ui/DeleteDialog.tsx index 8e3177e..394e0dd 100644 --- a/frontend/src/components/ui/DeleteDialog.tsx +++ b/frontend/src/components/ui/DeleteDialog.tsx @@ -14,14 +14,14 @@ const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false if (!isOpen) return null return ( -
+

{isGroup ? t('groups.confirmDelete') : t('server.confirmDelete')}

- {isGroup + {isGroup ? t('groups.deleteWarning', { name: serverName }) : t('server.deleteWarning', { name: serverName })}

diff --git a/frontend/src/contexts/ToastContext.tsx b/frontend/src/contexts/ToastContext.tsx index e8cd4e3..7760fdf 100644 --- a/frontend/src/contexts/ToastContext.tsx +++ b/frontend/src/contexts/ToastContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useState, ReactNode } from 'react'; +import React, { createContext, useContext, useState, ReactNode, useCallback } from 'react'; import Toast, { ToastType } from '@/components/ui/Toast'; interface ToastContextProps { @@ -32,18 +32,18 @@ export const ToastProvider: React.FC = ({ children }) => { duration: 3000, }); - const showToast = (message: string, type: ToastType = 'info', duration: number = 3000) => { + const showToast = useCallback((message: string, type: ToastType = 'info', duration: number = 3000) => { setToast({ message, type, visible: true, duration, }); - }; + }, []); - const hideToast = () => { + const hideToast = useCallback(() => { setToast((prev) => ({ ...prev, visible: false })); - }; + }, []); return ( diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts index 12512d3..1926a74 100644 --- a/frontend/src/hooks/useSettingsData.ts +++ b/frontend/src/hooks/useSettingsData.ts @@ -16,10 +16,19 @@ interface InstallConfig { npmRegistry: string; } +interface SmartRoutingConfig { + enabled: boolean; + dbUrl: string; + openaiApiBaseUrl: string; + openaiApiKey: string; + openaiApiEmbeddingModel: string; +} + interface SystemSettings { systemConfig?: { routing?: RoutingConfig; install?: InstallConfig; + smartRouting?: SmartRoutingConfig; }; } @@ -47,6 +56,14 @@ export const useSettingsData = () => { npmRegistry: '', }); + const [smartRoutingConfig, setSmartRoutingConfig] = useState({ + enabled: false, + dbUrl: '', + openaiApiBaseUrl: '', + openaiApiKey: '', + openaiApiEmbeddingModel: '', + }); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); @@ -89,14 +106,25 @@ export const useSettingsData = () => { npmRegistry: data.data.systemConfig.install.npmRegistry || '', }); } + if (data.success && data.data?.systemConfig?.smartRouting) { + setSmartRoutingConfig({ + enabled: data.data.systemConfig.smartRouting.enabled ?? false, + dbUrl: data.data.systemConfig.smartRouting.dbUrl || '', + openaiApiBaseUrl: data.data.systemConfig.smartRouting.openaiApiBaseUrl || '', + openaiApiKey: data.data.systemConfig.smartRouting.openaiApiKey || '', + openaiApiEmbeddingModel: + data.data.systemConfig.smartRouting.openaiApiEmbeddingModel || '', + }); + } } catch (error) { console.error('Failed to fetch settings:', error); setError(error instanceof Error ? error.message : 'Failed to fetch settings'); + // 使用一个稳定的 showToast 引用,避免将其加入依赖数组 showToast(t('errors.failedToFetchSettings')); } finally { setLoading(false); } - }, [t, showToast]); + }, [t]); // 移除 showToast 依赖 // Update routing configuration const updateRoutingConfig = async ( @@ -195,6 +223,107 @@ export const useSettingsData = () => { } }; + // Update smart routing configuration + const updateSmartRoutingConfig = async ( + key: T, + value: SmartRoutingConfig[T], + ) => { + setLoading(true); + setError(null); + + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch('/api/system-config', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': token || '', + }, + body: JSON.stringify({ + smartRouting: { + [key]: value, + }, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + if (data.success) { + setSmartRoutingConfig({ + ...smartRoutingConfig, + [key]: value, + }); + showToast(t('settings.systemConfigUpdated')); + return true; + } else { + showToast(data.message || t('errors.failedToUpdateSmartRoutingConfig')); + return false; + } + } catch (error) { + console.error('Failed to update smart routing config:', error); + const errorMessage = + error instanceof Error ? error.message : 'Failed to update smart routing config'; + setError(errorMessage); + showToast(errorMessage); + return false; + } finally { + setLoading(false); + } + }; + + // Update multiple smart routing configuration fields at once + const updateSmartRoutingConfigBatch = async (updates: Partial) => { + setLoading(true); + setError(null); + + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch('/api/system-config', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': token || '', + }, + body: JSON.stringify({ + smartRouting: updates, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + if (data.success) { + setSmartRoutingConfig({ + ...smartRoutingConfig, + ...updates, + }); + showToast(t('settings.systemConfigUpdated')); + return true; + } else { + showToast(data.message || t('errors.failedToUpdateSmartRoutingConfig')); + return false; + } + } catch (error) { + console.error('Failed to update smart routing config:', error); + const errorMessage = + error instanceof Error ? error.message : 'Failed to update smart routing config'; + setError(errorMessage); + showToast(errorMessage); + return false; + } finally { + setLoading(false); + } + }; + // Fetch settings when the component mounts or refreshKey changes useEffect(() => { fetchSettings(); @@ -213,6 +342,7 @@ export const useSettingsData = () => { tempRoutingConfig, setTempRoutingConfig, installConfig, + smartRoutingConfig, loading, error, setError, @@ -220,5 +350,7 @@ export const useSettingsData = () => { fetchSettings, updateRoutingConfig, updateInstallConfig, + updateSmartRoutingConfig, + updateSmartRoutingConfigBatch, }; }; diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index e9c41ad..c41c394 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -125,7 +125,8 @@ "initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch...", "serverInstall": "Failed to install server", "failedToFetchSettings": "Failed to fetch settings", - "failedToUpdateRouteConfig": "Failed to update route configuration" + "failedToUpdateRouteConfig": "Failed to update route configuration", + "failedToUpdateSmartRoutingConfig": "Failed to update smart routing configuration" }, "common": { "processing": "Processing...", @@ -170,8 +171,9 @@ "account": "Account Settings", "password": "Change Password", "appearance": "Appearance", - "routeConfig": "Security Configuration", - "installConfig": "Installation Configuration" + "routeConfig": "Security", + "installConfig": "Installation", + "smartRouting": "Smart Routing" }, "market": { "title": "Server Market - (Data from mcpm.sh)" @@ -277,7 +279,20 @@ "npmRegistry": "NPM Registry URL", "npmRegistryDescription": "Set npm_config_registry environment variable for NPM package installation", "npmRegistryPlaceholder": "e.g. https://registry.npmjs.org/", - "installConfig": "Installation Configuration", - "systemConfigUpdated": "System configuration updated successfully" + "installConfig": "Installation", + "systemConfigUpdated": "System configuration updated successfully", + "enableSmartRouting": "Enable Smart Routing", + "enableSmartRoutingDescription": "Enable smart routing feature to search the most suitable tool based on input (using $smart group name)", + "dbUrl": "PostgreSQL URL (with pgvector support)", + "dbUrlPlaceholder": "e.g. postgresql://user:password@localhost:5432/dbname", + "openaiApiBaseUrl": "OpenAI API Base URL", + "openaiApiBaseUrlPlaceholder": "https://api.openai.com/v1", + "openaiApiKey": "OpenAI API Key", + "openaiApiKeyPlaceholder": "Enter OpenAI API key", + "openaiApiEmbeddingModel": "OpenAI Embedding Model", + "openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small", + "smartRoutingConfigUpdated": "Smart routing configuration updated successfully", + "smartRoutingRequiredFields": "Database URL and OpenAI API Key are required to enable smart routing", + "smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}" } } \ No newline at end of file diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index e89105c..0a859a2 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -126,7 +126,8 @@ "serverInstall": "安装服务器失败", "failedToFetchSettings": "获取设置失败", "failedToUpdateSystemConfig": "更新系统配置失败", - "failedToUpdateRouteConfig": "更新路由配置失败" + "failedToUpdateRouteConfig": "更新路由配置失败", + "failedToUpdateSmartRoutingConfig": "更新智能路由配置失败" }, "common": { "processing": "处理中...", @@ -169,7 +170,8 @@ "password": "修改密码", "appearance": "外观", "routeConfig": "安全配置", - "installConfig": "安装配置" + "installConfig": "安装", + "smartRouting": "智能路由" }, "groups": { "title": "分组管理" @@ -279,6 +281,20 @@ "npmRegistryDescription": "设置 npm_config_registry 环境变量,用于 NPM 包安装", "npmRegistryPlaceholder": "例如: https://registry.npmmirror.com/", "installConfig": "安装配置", - "systemConfigUpdated": "系统配置更新成功" + "systemConfigUpdated": "系统配置更新成功", + "enableSmartRouting": "启用智能路由", + "enableSmartRoutingDescription": "开启智能路由功能,根据输入自动搜索最合适的工具", + "dbUrl": "PostgreSQL 连接地址(支持 pgvector)", + "dbUrlPlaceholder": "例如: postgresql://user:password@localhost:5432/dbname", + "openaiApiBaseUrl": "OpenAI API 基础地址", + "openaiApiBaseUrlPlaceholder": "https://api.openai.com/v1", + "openaiApiKey": "OpenAI API 密钥", + "openaiApiKeyDescription": "用于访问 OpenAI API 的密钥", + "openaiApiKeyPlaceholder": "请输入 OpenAI API 密钥", + "openaiApiEmbeddingModel": "OpenAI 嵌入模型", + "openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small", + "smartRoutingConfigUpdated": "智能路由配置更新成功", + "smartRoutingRequiredFields": "启用智能路由需要填写数据库连接地址和 OpenAI API 密钥", + "smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}" } } \ No newline at end of file diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 86187dc..a89be0e 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -26,14 +26,29 @@ const SettingsPage: React.FC = () => { npmRegistry: '', }); + const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{ + dbUrl: string; + openaiApiBaseUrl: string; + openaiApiKey: string; + openaiApiEmbeddingModel: string; + }>({ + dbUrl: '', + openaiApiBaseUrl: '', + openaiApiKey: '', + openaiApiEmbeddingModel: '', + }); + const { routingConfig, tempRoutingConfig, setTempRoutingConfig, installConfig: savedInstallConfig, + smartRoutingConfig, loading, updateRoutingConfig, - updateInstallConfig + updateInstallConfig, + updateSmartRoutingConfig, + updateSmartRoutingConfigBatch } = useSettingsData(); // Update local installConfig when savedInstallConfig changes @@ -43,13 +58,26 @@ const SettingsPage: React.FC = () => { } }, [savedInstallConfig]); + // Update local tempSmartRoutingConfig when smartRoutingConfig changes + useEffect(() => { + if (smartRoutingConfig) { + setTempSmartRoutingConfig({ + dbUrl: smartRoutingConfig.dbUrl || '', + openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '', + openaiApiKey: smartRoutingConfig.openaiApiKey || '', + openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '', + }); + } + }, [smartRoutingConfig]); + const [sectionsVisible, setSectionsVisible] = useState({ routingConfig: false, installConfig: false, + smartRoutingConfig: false, password: false }); - const toggleSection = (section: 'routingConfig' | 'installConfig' | 'password') => { + const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'password') => { setSectionsVisible(prev => ({ ...prev, [section]: !prev[section] @@ -91,6 +119,59 @@ const SettingsPage: React.FC = () => { await updateInstallConfig(key, installConfig[key]); }; + const handleSmartRoutingConfigChange = (key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel', value: string) => { + setTempSmartRoutingConfig({ + ...tempSmartRoutingConfig, + [key]: value + }); + }; + + const saveSmartRoutingConfig = async (key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel') => { + await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]); + }; + + const handleSmartRoutingEnabledChange = async (value: boolean) => { + // If enabling Smart Routing, validate required fields and save any unsaved changes + if (value) { + const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl; + const currentOpenaiApiKey = tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey; + + if (!currentDbUrl || !currentOpenaiApiKey) { + const missingFields = []; + if (!currentDbUrl) missingFields.push(t('settings.dbUrl')); + if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey')); + + showToast(t('settings.smartRoutingValidationError', { + fields: missingFields.join(', ') + })); + return; + } + + // Prepare updates object with unsaved changes and enabled status + const updates: any = { enabled: value }; + + // Check for unsaved changes and include them in the batch update + if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) { + updates.dbUrl = tempSmartRoutingConfig.dbUrl; + } + if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) { + updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl; + } + if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) { + updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey; + } + if (tempSmartRoutingConfig.openaiApiEmbeddingModel !== smartRoutingConfig.openaiApiEmbeddingModel) { + updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel; + } + + // Save all changes in a single batch update + await updateSmartRoutingConfigBatch(updates); + } else { + // If disabling, just update the enabled status + await updateSmartRoutingConfig('enabled', value); + } + }; + const handlePasswordChangeSuccess = () => { setTimeout(() => { navigate('/'); @@ -133,6 +214,131 @@ const SettingsPage: React.FC = () => {
+ {/* Smart Routing Configuration Settings */} +
+
toggleSection('smartRoutingConfig')} + > +

{t('pages.settings.smartRouting')}

+ + {sectionsVisible.smartRoutingConfig ? '▼' : '►'} + +
+ + {sectionsVisible.smartRoutingConfig && ( +
+
+
+

{t('settings.enableSmartRouting')}

+

{t('settings.enableSmartRoutingDescription')}

+
+ handleSmartRoutingEnabledChange(checked)} + /> +
+ +
+
+

+ *{t('settings.dbUrl')} +

+
+
+ handleSmartRoutingConfigChange('dbUrl', e.target.value)} + placeholder={t('settings.dbUrlPlaceholder')} + className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300" + disabled={loading} + /> + +
+
+ +
+
+

+ *{t('settings.openaiApiKey')} +

+
+
+ handleSmartRoutingConfigChange('openaiApiKey', e.target.value)} + placeholder={t('settings.openaiApiKeyPlaceholder')} + className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300" + disabled={loading} + /> + +
+
+ +
+
+

{t('settings.openaiApiBaseUrl')}

+
+
+ handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)} + placeholder={t('settings.openaiApiBaseUrlPlaceholder')} + className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + disabled={loading} + /> + +
+
+ +
+
+

{t('settings.openaiApiEmbeddingModel')}

+
+
+ handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)} + placeholder={t('settings.openaiApiEmbeddingModelPlaceholder')} + className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + disabled={loading} + /> + +
+
+
+ )} +
+ {/* Route Configuration Settings */}
{
)}
-
+
); }; diff --git a/package.json b/package.json index 90688de..a8272f1 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,19 @@ "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.1", + "@types/pg": "^8.15.2", "bcryptjs": "^3.0.2", "dotenv": "^16.3.1", + "dotenv-expand": "^12.0.2", "express": "^4.21.2", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", + "openai": "^4.103.0", + "pg": "^8.16.0", + "pgvector": "^0.2.1", + "postgres": "^3.4.7", + "reflect-metadata": "^0.2.2", + "typeorm": "^0.3.24", "uuid": "^11.1.0" }, "devDependencies": { @@ -96,4 +104,4 @@ "node": "^18.0.0 || >=20.0.0" }, "packageManager": "pnpm@10.11.0+sha256.a69e9cb077da419d47d18f1dd52e207245b29cac6e076acedbeb8be3b1a67bd7" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fe51e2..2923ab8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,18 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.11.1 version: 1.11.5 + '@types/pg': + specifier: ^8.15.2 + version: 8.15.2 bcryptjs: specifier: ^3.0.2 version: 3.0.2 dotenv: specifier: ^16.3.1 version: 16.5.0 + dotenv-expand: + specifier: ^12.0.2 + version: 12.0.2 express: specifier: ^4.21.2 version: 4.21.2 @@ -26,6 +32,24 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 + openai: + specifier: ^4.103.0 + version: 4.103.0(zod@3.25.20) + pg: + specifier: ^8.16.0 + version: 8.16.0 + pgvector: + specifier: ^0.2.1 + version: 0.2.1 + postgres: + specifier: ^3.4.7 + version: 3.4.7 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + typeorm: + specifier: ^0.3.24 + version: 0.3.24(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.8.3)) uuid: specifier: ^11.1.0 version: 11.1.0 @@ -645,6 +669,10 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -811,6 +839,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} @@ -1064,6 +1096,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -1236,9 +1271,18 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + + '@types/node@18.19.103': + resolution: {integrity: sha512-hHTHp+sEz6SxFsp+SA+Tqrua3AbmlAw+Y//aEwdHrdZkYVRWdvWD3y5uPZ0flYOkgskaFWqZ/YGFm3FaFQ0pRw==} + '@types/node@22.15.21': resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==} + '@types/pg@8.15.2': + resolution: {integrity: sha512-+BKxo5mM6+/A1soSHBI7ufUglqYXntChLDyTbvcAn1Lawi9J7J9Ok3jt6w7I0+T/UDJ4CyhHk66+GZbwmkYxSg==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1347,6 +1391,10 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1369,6 +1417,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1395,10 +1447,22 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + ansis@3.17.0: + resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + engines: {node: '>=14'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -1418,6 +1482,9 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.21: resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} @@ -1611,6 +1678,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -1684,6 +1755,9 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1718,6 +1792,14 @@ packages: babel-plugin-macros: optional: true + dedent@1.6.0: + resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1728,6 +1810,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1760,6 +1846,10 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dotenv-expand@12.0.2: + resolution: {integrity: sha512-lXpXz2ZE1cea1gL4sz2Ipj8y4PiVjytYr3Ij0SWoms1PGxIv7m2CRKuRuCRtHdVuvM/hNJPMxt5PbhboNC4dPQ==} + engines: {node: '>=12'} + dotenv@16.5.0: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} @@ -1771,6 +1861,9 @@ packages: dynamic-dedupe@0.3.0: resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -1792,6 +1885,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -1819,6 +1915,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.2: resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} engines: {node: '>=18'} @@ -1882,6 +1982,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventsource-parser@3.0.1: resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} engines: {node: '>=18.0.0'} @@ -1992,6 +2096,21 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2061,6 +2180,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -2095,6 +2218,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2117,6 +2244,9 @@ packages: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + i18next-browser-languagedetector@8.1.0: resolution: {integrity: sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==} @@ -2251,6 +2381,9 @@ packages: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jake@10.9.2: resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} engines: {node: '>=10'} @@ -2562,6 +2695,9 @@ packages: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} engines: {node: '>=12'} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2657,6 +2793,10 @@ packages: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -2726,6 +2866,15 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2760,6 +2909,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2775,6 +2927,18 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + openai@4.103.0: + resolution: {integrity: sha512-eWcz9kdurkGOFDtd5ySS5y251H2uBgq9+1a2lTBnjMMzlexJ40Am5t6Mu76SSE87VvitPa0dkIAp75F+dZVC0g==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2803,6 +2967,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2834,6 +3001,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -2845,6 +3016,52 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pg-cloudflare@1.2.5: + resolution: {integrity: sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==} + + pg-connection-string@2.9.0: + resolution: {integrity: sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.10.0: + resolution: {integrity: sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.10.0: + resolution: {integrity: sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + + pg@8.16.0: + resolution: {integrity: sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + pgvector@0.2.1: + resolution: {integrity: sha512-nKaQY9wtuiidwLMdVIce1O3kL0d+FxrigCVzsShnoqzOSaWWWOvuctb/sYwlai5cTwwzRSNa+a/NtN2kVZGNJw==} + engines: {node: '>= 18'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2879,6 +3096,45 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2987,6 +3243,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -3100,6 +3359,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + sharp@0.34.2: resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3135,6 +3398,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -3162,9 +3429,17 @@ packages: spawn-command@0.0.2: resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sql-highlight@6.0.0: + resolution: {integrity: sha512-+fLpbAbWkQ+d0JEchJT/NrRRXbYRNbG15gFpANx73EwxQB1PRjj+k/OI0GTU0J63g8ikGkJECQp9z8XEJZvPRw==} + engines: {node: '>=14'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -3189,6 +3464,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -3290,6 +3569,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3388,11 +3670,73 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typeorm@0.3.24: + resolution: {integrity: sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==} + engines: {node: '>=16.13.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 + '@sap/hana-client': ^2.12.25 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + hdb-pool: ^0.1.6 + ioredis: ^5.0.4 + mongodb: ^5.8.0 || ^6.0.0 + mssql: ^9.1.1 || ^10.0.1 || ^11.0.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 + reflect-metadata: ^0.1.14 || ^0.2.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + hdb-pool: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -3493,6 +3837,16 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3506,6 +3860,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -3962,6 +4320,15 @@ snapshots: '@img/sharp-win32-x64@0.34.2': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -4214,6 +4581,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@pkgjs/parseargs@0.11.0': + optional: true + '@radix-ui/primitive@1.1.2': {} '@radix-ui/react-accordion@1.2.11(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': @@ -4414,6 +4784,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sqltools/formatter@1.2.5': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -4589,10 +4961,25 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.15.21 + form-data: 4.0.2 + + '@types/node@18.19.103': + dependencies: + undici-types: 5.26.5 + '@types/node@22.15.21': dependencies: undici-types: 6.21.0 + '@types/pg@8.15.2': + dependencies: + '@types/node': 22.15.21 + pg-protocol: 1.10.0 + pg-types: 4.0.2 + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -4731,6 +5118,10 @@ snapshots: transitivePeerDependencies: - supports-color + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -4751,6 +5142,10 @@ snapshots: acorn@8.14.1: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4779,11 +5174,17 @@ snapshots: ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} + + ansis@3.17.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 + app-root-path@3.1.0: {} + arg@4.1.3: {} argparse@1.0.10: @@ -4798,6 +5199,8 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + autoprefixer@10.4.21(postcss@8.5.3): dependencies: browserslist: 4.24.4 @@ -5040,6 +5443,10 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@10.0.1: {} concat-map@0.0.1: {} @@ -5114,6 +5521,8 @@ snapshots: dependencies: '@babel/runtime': 7.27.0 + dayjs@1.11.13: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -5128,6 +5537,8 @@ snapshots: dedent@1.5.3: {} + dedent@1.6.0: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -5136,6 +5547,8 @@ snapshots: dependencies: clone: 1.0.4 + delayed-stream@1.0.0: {} + depd@2.0.0: {} destroy@1.2.0: {} @@ -5156,6 +5569,10 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv-expand@12.0.2: + dependencies: + dotenv: 16.5.0 + dotenv@16.5.0: {} dunder-proto@1.0.1: @@ -5168,6 +5585,8 @@ snapshots: dependencies: xtend: 4.0.2 + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -5184,6 +5603,8 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -5205,6 +5626,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.25.2: optionalDependencies: '@esbuild/aix-ppc64': 0.25.2 @@ -5313,6 +5741,8 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + eventsource-parser@3.0.1: {} eventsource@3.0.6: @@ -5516,6 +5946,25 @@ snapshots: flatted@3.3.3: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data-encoder@1.7.2: {} + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -5579,6 +6028,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -5613,6 +6071,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -5635,6 +6097,10 @@ snapshots: human-signals@4.3.1: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + i18next-browser-languagedetector@8.1.0: dependencies: '@babel/runtime': 7.27.0 @@ -5758,6 +6224,12 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jake@10.9.2: dependencies: async: 3.2.6 @@ -6224,6 +6696,8 @@ snapshots: chalk: 5.2.0 is-unicode-supported: 1.3.0 + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -6297,6 +6771,10 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + minimist@1.2.8: {} minipass@7.1.2: {} @@ -6348,6 +6826,10 @@ snapshots: node-domexception@1.0.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -6374,6 +6856,8 @@ snapshots: object-inspect@1.13.4: {} + obuf@1.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -6390,6 +6874,20 @@ snapshots: dependencies: mimic-fn: 4.0.0 + openai@4.103.0(zod@3.25.20): + dependencies: + '@types/node': 18.19.103 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + zod: 3.25.20 + transitivePeerDependencies: + - encoding + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6429,6 +6927,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6452,12 +6952,66 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + path-to-regexp@0.1.12: {} path-to-regexp@8.2.0: {} path-type@4.0.0: {} + pg-cloudflare@1.2.5: + optional: true + + pg-connection-string@2.9.0: {} + + pg-int8@1.0.1: {} + + pg-numeric@1.0.2: {} + + pg-pool@3.10.0(pg@8.16.0): + dependencies: + pg: 8.16.0 + + pg-protocol@1.10.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.4 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.16.0: + dependencies: + pg-connection-string: 2.9.0 + pg-pool: 3.10.0(pg@8.16.0) + pg-protocol: 1.10.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.2.5 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + pgvector@0.2.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -6486,6 +7040,30 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.0: {} + + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + + postgres@3.4.7: {} + prelude-ls@1.2.1: {} prettier@3.5.3: {} @@ -6581,6 +7159,8 @@ snapshots: dependencies: picomatch: 2.3.1 + reflect-metadata@0.2.2: {} + regenerator-runtime@0.14.1: {} require-directory@2.1.1: {} @@ -6732,6 +7312,11 @@ snapshots: setprototypeof@1.2.0: {} + sha.js@2.4.11: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + sharp@0.34.2: dependencies: color: 4.2.3 @@ -6799,6 +7384,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 @@ -6824,8 +7411,12 @@ snapshots: spawn-command@0.0.2: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} + sql-highlight@6.0.0: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -6849,6 +7440,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -6930,6 +7527,8 @@ snapshots: toidentifier@1.0.1: {} + tr46@0.0.3: {} + tree-kill@1.2.2: {} ts-api-utils@1.4.3(typescript@5.8.3): @@ -7031,8 +7630,34 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 + typeorm@0.3.24(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.8.3)): + dependencies: + '@sqltools/formatter': 1.2.5 + ansis: 3.17.0 + app-root-path: 3.1.0 + buffer: 6.0.3 + dayjs: 1.11.13 + debug: 4.4.1 + dedent: 1.6.0 + dotenv: 16.5.0 + glob: 10.4.5 + reflect-metadata: 0.2.2 + sha.js: 2.4.11 + sql-highlight: 6.0.0 + tslib: 2.8.1 + uuid: 11.1.0 + yargs: 17.7.2 + optionalDependencies: + pg: 8.16.0 + ts-node: 10.9.2(@types/node@22.15.21)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + typescript@5.8.3: {} + undici-types@5.26.5: {} + undici-types@6.21.0: {} universalify@2.0.1: {} @@ -7094,6 +7719,15 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -7106,6 +7740,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} write-file-atomic@4.0.2: diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 33fc9a7..2864acd 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -9,6 +9,7 @@ import { toggleServerStatus, } from '../services/mcpService.js'; import { loadSettings, saveSettings } from '../config/index.js'; +import { syncAllServerToolsEmbeddings } from '../services/vectorSearchService.js'; export const getAllServers = (_: Request, res: Response): void => { try { @@ -283,7 +284,7 @@ export const toggleServer = async (req: Request, res: Response): Promise = export const updateSystemConfig = (req: Request, res: Response): void => { try { - const { routing, install } = req.body; + const { routing, install, smartRouting } = req.body; if ( (!routing || @@ -292,7 +293,13 @@ export const updateSystemConfig = (req: Request, res: Response): void => { typeof routing.enableBearerAuth !== 'boolean' && typeof routing.bearerAuthKey !== 'string')) && (!install || - (typeof install.pythonIndexUrl !== 'string' && typeof install.npmRegistry !== 'string')) + (typeof install.pythonIndexUrl !== 'string' && typeof install.npmRegistry !== 'string')) && + (!smartRouting || + (typeof smartRouting.enabled !== 'boolean' && + typeof smartRouting.dbUrl !== 'string' && + typeof smartRouting.openaiApiBaseUrl !== 'string' && + typeof smartRouting.openaiApiKey !== 'string' && + typeof smartRouting.openaiApiEmbeddingModel !== 'string')) ) { res.status(400).json({ success: false, @@ -314,6 +321,13 @@ export const updateSystemConfig = (req: Request, res: Response): void => { pythonIndexUrl: '', npmRegistry: '', }, + smartRouting: { + enabled: false, + dbUrl: '', + openaiApiBaseUrl: '', + openaiApiKey: '', + openaiApiEmbeddingModel: '', + }, }; } @@ -333,6 +347,16 @@ export const updateSystemConfig = (req: Request, res: Response): void => { }; } + if (!settings.systemConfig.smartRouting) { + settings.systemConfig.smartRouting = { + enabled: false, + dbUrl: '', + openaiApiBaseUrl: '', + openaiApiKey: '', + openaiApiEmbeddingModel: '', + }; + } + if (routing) { if (typeof routing.enableGlobalRoute === 'boolean') { settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute; @@ -360,12 +384,77 @@ export const updateSystemConfig = (req: Request, res: Response): void => { } } + // Track smartRouting state and configuration changes + const wasSmartRoutingEnabled = settings.systemConfig.smartRouting.enabled || false; + const previousSmartRoutingConfig = { ...settings.systemConfig.smartRouting }; + let needsSync = false; + + if (smartRouting) { + if (typeof smartRouting.enabled === 'boolean') { + // If enabling Smart Routing, validate required fields + if (smartRouting.enabled) { + const currentDbUrl = smartRouting.dbUrl || settings.systemConfig.smartRouting.dbUrl; + const currentOpenaiApiKey = + smartRouting.openaiApiKey || settings.systemConfig.smartRouting.openaiApiKey; + + if (!currentDbUrl || !currentOpenaiApiKey) { + const missingFields = []; + if (!currentDbUrl) missingFields.push('Database URL'); + if (!currentOpenaiApiKey) missingFields.push('OpenAI API Key'); + + res.status(400).json({ + success: false, + message: `Smart Routing requires the following fields: ${missingFields.join(', ')}`, + }); + return; + } + } + settings.systemConfig.smartRouting.enabled = smartRouting.enabled; + } + if (typeof smartRouting.dbUrl === 'string') { + settings.systemConfig.smartRouting.dbUrl = smartRouting.dbUrl; + } + if (typeof smartRouting.openaiApiBaseUrl === 'string') { + settings.systemConfig.smartRouting.openaiApiBaseUrl = smartRouting.openaiApiBaseUrl; + } + if (typeof smartRouting.openaiApiKey === 'string') { + settings.systemConfig.smartRouting.openaiApiKey = smartRouting.openaiApiKey; + } + if (typeof smartRouting.openaiApiEmbeddingModel === 'string') { + settings.systemConfig.smartRouting.openaiApiEmbeddingModel = + smartRouting.openaiApiEmbeddingModel; + } + + // Check if we need to sync embeddings + const isNowEnabled = settings.systemConfig.smartRouting.enabled || false; + const hasConfigChanged = + previousSmartRoutingConfig.dbUrl !== settings.systemConfig.smartRouting.dbUrl || + previousSmartRoutingConfig.openaiApiBaseUrl !== + settings.systemConfig.smartRouting.openaiApiBaseUrl || + previousSmartRoutingConfig.openaiApiKey !== + settings.systemConfig.smartRouting.openaiApiKey || + previousSmartRoutingConfig.openaiApiEmbeddingModel !== + settings.systemConfig.smartRouting.openaiApiEmbeddingModel; + + // Sync if: first time enabling OR smart routing is enabled and any config changed + needsSync = (!wasSmartRoutingEnabled && isNowEnabled) || (isNowEnabled && hasConfigChanged); + } + if (saveSettings(settings)) { res.json({ success: true, data: settings.systemConfig, message: 'System configuration updated successfully', }); + + // If smart routing configuration changed, sync all existing server tools + if (needsSync) { + console.log('SmartRouting configuration changed - syncing all existing server tools...'); + // Run sync asynchronously to avoid blocking the response + syncAllServerToolsEmbeddings().catch((error) => { + console.error('Failed to sync server tools embeddings:', error); + }); + } } else { res.status(500).json({ success: false, diff --git a/src/db/connection.ts b/src/db/connection.ts new file mode 100644 index 0000000..b3fa3e2 --- /dev/null +++ b/src/db/connection.ts @@ -0,0 +1,318 @@ +import 'reflect-metadata'; // Ensure reflect-metadata is imported here too +import { DataSource, DataSourceOptions } from 'typeorm'; +import dotenv from 'dotenv'; +import dotenvExpand from 'dotenv-expand'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import entities from './entities/index.js'; +import { registerPostgresVectorType } from './types/postgresVectorType.js'; +import { VectorEmbeddingSubscriber } from './subscribers/VectorEmbeddingSubscriber.js'; +import { loadSettings } from '../config/index.js'; + +// Get database URL from smart routing config or fallback to environment variable +const getDatabaseUrl = (): string => { + try { + const settings = loadSettings(); + const smartRouting = settings.systemConfig?.smartRouting; + + // Use smart routing dbUrl if smart routing is enabled and dbUrl is configured + if (smartRouting?.enabled && smartRouting?.dbUrl) { + console.log('Using smart routing database URL'); + return smartRouting.dbUrl; + } + } catch (error) { + console.warn( + 'Failed to load settings for smart routing database URL, falling back to environment variable:', + error, + ); + } + + return ''; +}; + +// Default database configuration +const defaultConfig: DataSourceOptions = { + type: 'postgres', + url: getDatabaseUrl(), + synchronize: true, + entities: entities, + subscribers: [VectorEmbeddingSubscriber], +}; + +// AppDataSource is the TypeORM data source +let AppDataSource = new DataSource(defaultConfig); + +// Function to create a new DataSource with updated configuration +export const updateDataSourceConfig = (): DataSource => { + const newConfig: DataSourceOptions = { + ...defaultConfig, + url: getDatabaseUrl(), + }; + + // If the configuration has changed, we need to create a new DataSource + const currentUrl = (AppDataSource.options as any).url; + if (currentUrl !== newConfig.url) { + console.log('Database URL configuration changed, updating DataSource...'); + AppDataSource = new DataSource(newConfig); + } + + return AppDataSource; +}; + +// Get the current AppDataSource instance +export const getAppDataSource = (): DataSource => { + return AppDataSource; +}; + +// Reconnect database with updated configuration +export const reconnectDatabase = async (): Promise => { + try { + // Close existing connection if it exists + if (AppDataSource.isInitialized) { + console.log('Closing existing database connection...'); + await AppDataSource.destroy(); + } + + // Update configuration and reconnect + AppDataSource = updateDataSourceConfig(); + return await initializeDatabase(); + } catch (error) { + console.error('Error during database reconnection:', error); + throw error; + } +}; + +// Initialize database connection +export const initializeDatabase = async (): Promise => { + try { + // Update configuration before initializing + AppDataSource = updateDataSourceConfig(); + + if (!AppDataSource.isInitialized) { + console.log('Initializing database connection...'); + await AppDataSource.initialize(); + + // Register the vector type with TypeORM + registerPostgresVectorType(AppDataSource); + + // Create pgvector extension if it doesn't exist + await AppDataSource.query('CREATE EXTENSION IF NOT EXISTS vector;').catch((err) => { + console.warn('Failed to create vector extension:', err.message); + console.warn('Vector functionality may not be available.'); + }); + + // Set up vector column and index with a more direct approach + try { + // First, create the extension + await AppDataSource.query(`CREATE EXTENSION IF NOT EXISTS vector;`); + + // Check if table exists first + const tableExists = await AppDataSource.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'vector_embeddings' + ); + `); + + if (tableExists[0].exists) { + // Add pgvector support via raw SQL commands + console.log('Configuring vector support for embeddings table...'); + + // Step 1: Drop any existing index on the column + try { + await AppDataSource.query(`DROP INDEX IF EXISTS idx_vector_embeddings_embedding;`); + } catch (dropError: any) { + console.warn('Note: Could not drop existing index:', dropError.message); + } + + // Step 2: Alter column type to vector (if it's not already) + try { + // Check column type first + const columnType = await AppDataSource.query(` + SELECT data_type FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'vector_embeddings' + AND column_name = 'embedding'; + `); + + if (columnType.length > 0 && columnType[0].data_type !== 'vector') { + await AppDataSource.query(` + ALTER TABLE vector_embeddings + ALTER COLUMN embedding TYPE vector USING embedding::vector; + `); + console.log('Vector embedding column type updated successfully.'); + } + } catch (alterError: any) { + console.warn('Could not alter embedding column type:', alterError.message); + console.warn('Will try to recreate the table later.'); + } + + // Step 3: Try to create appropriate indices + try { + // First, let's check if there are any records to determine the dimensions + const records = await AppDataSource.query(` + SELECT dimensions FROM vector_embeddings LIMIT 1; + `); + + let dimensions = 1536; // Default to common OpenAI embedding size + if (records && records.length > 0 && records[0].dimensions) { + dimensions = records[0].dimensions; + console.log(`Found vector dimension from existing data: ${dimensions}`); + } else { + console.log(`Using default vector dimension: ${dimensions} (no existing data found)`); + } + + // Set the vector dimensions explicitly only if table has data + if (records && records.length > 0) { + await AppDataSource.query(` + ALTER TABLE vector_embeddings + ALTER COLUMN embedding TYPE vector(${dimensions}); + `); + + // Now try to create the index + await AppDataSource.query(` + CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding + ON vector_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + `); + console.log('Created IVFFlat index for vector similarity search.'); + } else { + console.log( + 'No existing vector data found, skipping index creation - will be handled by vector service.', + ); + } + } catch (indexError: any) { + console.warn('IVFFlat index creation failed:', indexError.message); + console.warn('Trying alternative index type...'); + + try { + // Try HNSW index instead + await AppDataSource.query(` + CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding + ON vector_embeddings USING hnsw (embedding vector_cosine_ops); + `); + console.log('Created HNSW index for vector similarity search.'); + } catch (hnswError: any) { + // Final fallback to simpler index type + console.warn('HNSW index creation failed too. Using simple L2 distance index.'); + + try { + // Create a basic GIN index as last resort + await AppDataSource.query(` + CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding + ON vector_embeddings USING gin (embedding); + `); + console.log('Created GIN index for basic vector lookups.'); + } catch (ginError: any) { + console.warn('All index creation attempts failed:', ginError.message); + console.warn('Vector search will be slower without an optimized index.'); + } + } + } + } else { + console.log( + 'Vector embeddings table does not exist yet - will configure after schema sync.', + ); + } + } catch (error: any) { + console.warn('Could not set up vector column/index:', error.message); + console.warn('Will attempt again after schema synchronization.'); + } + + console.log('Database connection established successfully.'); + + // Run one final setup check after schema synchronization is done + if (defaultConfig.synchronize) { + setTimeout(async () => { + try { + console.log('Running final vector configuration check...'); + + // Try setup again with the same code from above + const tableExists = await AppDataSource.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'vector_embeddings' + ); + `); + + if (tableExists[0].exists) { + console.log('Vector embeddings table found, checking configuration...'); + + // Get the dimension size first + try { + // Try to get dimensions from an existing record + const records = await AppDataSource.query(` + SELECT dimensions FROM vector_embeddings LIMIT 1; + `); + + // Only proceed if we have existing data, otherwise let vector service handle it + if (records && records.length > 0 && records[0].dimensions) { + const dimensions = records[0].dimensions; + console.log(`Found vector dimension from database: ${dimensions}`); + + // Ensure column type is vector with explicit dimensions + await AppDataSource.query(` + ALTER TABLE vector_embeddings + ALTER COLUMN embedding TYPE vector(${dimensions}); + `); + console.log('Vector embedding column type updated in final check.'); + + // One more attempt at creating the index with dimensions + try { + // Drop existing index if any + await AppDataSource.query(` + DROP INDEX IF EXISTS idx_vector_embeddings_embedding; + `); + + // Create new index with proper dimensions + await AppDataSource.query(` + CREATE INDEX idx_vector_embeddings_embedding + ON vector_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + `); + console.log('Created IVFFlat index in final check.'); + } catch (indexError: any) { + console.warn( + 'Final index creation attempt did not succeed:', + indexError.message, + ); + console.warn('Using basic lookup without vector index.'); + } + } else { + console.log( + 'No existing vector data found, vector dimensions will be configured by vector service.', + ); + } + } catch (setupError: any) { + console.warn('Vector setup in final check failed:', setupError.message); + } + } + } catch (error: any) { + console.warn('Post-initialization vector setup failed:', error.message); + } + }, 3000); // Give synchronize some time to complete + } + } + return AppDataSource; + } catch (error) { + console.error('Error during database initialization:', error); + throw error; + } +}; + +// Get database connection status +export const isDatabaseConnected = (): boolean => { + return AppDataSource.isInitialized; +}; + +// Close database connection +export const closeDatabase = async (): Promise => { + if (AppDataSource.isInitialized) { + await AppDataSource.destroy(); + console.log('Database connection closed.'); + } +}; + +// Export AppDataSource for backward compatibility +export { AppDataSource }; + +export default getAppDataSource; diff --git a/src/db/entities/VectorEmbedding.ts b/src/db/entities/VectorEmbedding.ts new file mode 100644 index 0000000..67f30cb --- /dev/null +++ b/src/db/entities/VectorEmbedding.ts @@ -0,0 +1,46 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'vector_embeddings' }) +export class VectorEmbedding { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar' }) + content_type: string; // 'market_server', 'tool', 'documentation', etc. + + @Column({ type: 'varchar' }) + content_id: string; // Reference ID to the original content + + @Column('text') + text_content: string; // The text that was embedded + + @Column('simple-json') + metadata: Record; // Additional metadata about the embedding + + @Column({ + type: 'float', + array: true, + nullable: true, + }) + embedding: number[]; // The vector embedding - will be converted to vector type after table creation + + @Column({ type: 'int' }) + dimensions: number; // Dimensionality of the embedding vector + + @Column({ type: 'varchar' }) + model: string; // Model used to create the embedding + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' }) + updatedAt: Date; +} + +export default VectorEmbedding; diff --git a/src/db/entities/index.ts b/src/db/entities/index.ts new file mode 100644 index 0000000..2fccf16 --- /dev/null +++ b/src/db/entities/index.ts @@ -0,0 +1,7 @@ +import { VectorEmbedding } from './VectorEmbedding.js'; + +// Export all entities +export default [VectorEmbedding]; + +// Export individual entities for direct use +export { VectorEmbedding }; diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..f8bcba7 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,33 @@ +import { initializeDatabase, closeDatabase, isDatabaseConnected } from './connection.js'; +import * as repositories from './repositories/index.js'; + +/** + * Initialize the database module + */ +export async function initializeDbModule(): Promise { + try { + // Connect to the database + await initializeDatabase(); + return true; + } catch (error) { + console.error('Failed to initialize database module:', error); + return false; + } +} + +/** + * Get the repository factory for a database entity type + * @param entityType The type of entity to get a repository for + */ +export function getRepositoryFactory(entityType: 'vectorEmbeddings') { + // Return the appropriate repository based on entity type + switch (entityType) { + case 'vectorEmbeddings': + return () => new repositories.VectorEmbeddingRepository(); + default: + throw new Error(`Unknown entity type: ${entityType}`); + } +} + +// Re-export everything from the database module +export { initializeDatabase, closeDatabase, isDatabaseConnected, repositories }; diff --git a/src/db/repositories/BaseRepository.ts b/src/db/repositories/BaseRepository.ts new file mode 100644 index 0000000..5c4e748 --- /dev/null +++ b/src/db/repositories/BaseRepository.ts @@ -0,0 +1,69 @@ +import { Repository, EntityTarget, ObjectLiteral } from 'typeorm'; +import { getAppDataSource } from '../connection.js'; + +/** + * Base repository class with common CRUD operations + */ +export class BaseRepository { + protected readonly repository: Repository; + + constructor(entityClass: EntityTarget) { + this.repository = getAppDataSource().getRepository(entityClass); + } + + /** + * Get repository access + */ + getRepository(): Repository { + return this.repository; + } + + /** + * Find all entities + */ + async findAll(): Promise { + return this.repository.find(); + } + + /** + * Find entity by ID + * @param id Entity ID + */ + async findById(id: string | number): Promise { + return this.repository.findOneBy({ id } as any); + } + + /** + * Save or update an entity + * @param entity Entity to save + */ + async save(entity: Partial): Promise { + return this.repository.save(entity as any); + } + + /** + * Save multiple entities + * @param entities Array of entities to save + */ + async saveMany(entities: Partial[]): Promise { + return this.repository.save(entities as any[]); + } + + /** + * Delete an entity by ID + * @param id Entity ID + */ + async delete(id: string | number): Promise { + const result = await this.repository.delete(id); + return result.affected !== null && result.affected !== undefined && result.affected > 0; + } + + /** + * Count total entities + */ + async count(): Promise { + return this.repository.count(); + } +} + +export default BaseRepository; diff --git a/src/db/repositories/VectorEmbeddingRepository.ts b/src/db/repositories/VectorEmbeddingRepository.ts new file mode 100644 index 0000000..a3b01ba --- /dev/null +++ b/src/db/repositories/VectorEmbeddingRepository.ts @@ -0,0 +1,219 @@ +import { VectorEmbedding } from '../entities/VectorEmbedding.js'; +import BaseRepository from './BaseRepository.js'; +import { getAppDataSource } from '../connection.js'; + +export class VectorEmbeddingRepository extends BaseRepository { + constructor() { + super(VectorEmbedding); + } + + /** + * Find by content type and ID + * @param contentType Content type + * @param contentId Content ID + */ + async findByContentIdentity( + contentType: string, + contentId: string, + ): Promise { + return this.repository.findOneBy({ + content_type: contentType, + content_id: contentId, + }); + } + + /** + * Create or update an embedding for content + * @param contentType Content type + * @param contentId Content ID + * @param textContent Text content to embed + * @param embedding Vector embedding + * @param metadata Additional metadata + * @param model Model used to create the embedding + */ + async saveEmbedding( + contentType: string, + contentId: string, + textContent: string, + embedding: number[], + metadata: Record = {}, + model = 'default', + ): Promise { + // Check if embedding exists + let vectorEmbedding = await this.findByContentIdentity(contentType, contentId); + + if (!vectorEmbedding) { + vectorEmbedding = new VectorEmbedding(); + vectorEmbedding.content_type = contentType; + vectorEmbedding.content_id = contentId; + } + + // Update properties + vectorEmbedding.text_content = textContent; + vectorEmbedding.embedding = embedding; + vectorEmbedding.dimensions = embedding.length; + vectorEmbedding.metadata = metadata; + vectorEmbedding.model = model; + + // For raw SQL operations where our subscriber might not be called + // Ensure the embedding is properly formatted for postgres + const rawEmbedding = this.formatEmbeddingForPgVector(embedding); + if (rawEmbedding) { + (vectorEmbedding as any).embedding = rawEmbedding; + } + + return this.save(vectorEmbedding); + } + + /** + * Search for similar embeddings using cosine similarity + * @param embedding Vector embedding to search against + * @param limit Maximum number of results (default: 10) + * @param threshold Similarity threshold (default: 0.7) + * @param contentTypes Optional content types to filter by + */ + async searchSimilar( + embedding: number[], + limit = 10, + threshold = 0.7, + contentTypes?: string[], + ): Promise> { + try { + // Try using vector similarity operator first + try { + // Build query with vector operators + let query = getAppDataSource() + .createQueryBuilder() + .select('vector_embedding.*') + .addSelect(`1 - (vector_embedding.embedding <=> :embedding) AS similarity`) + .from(VectorEmbedding, 'vector_embedding') + .where(`1 - (vector_embedding.embedding <=> :embedding) > :threshold`) + .orderBy('similarity', 'DESC') + .limit(limit) + .setParameter( + 'embedding', + Array.isArray(embedding) ? `[${embedding.join(',')}]` : embedding, + ) + .setParameter('threshold', threshold); + + // Add content type filter if provided + if (contentTypes && contentTypes.length > 0) { + query = query + .andWhere('vector_embedding.content_type IN (:...contentTypes)') + .setParameter('contentTypes', contentTypes); + } + + // Execute query + const results = await query.getRawMany(); + + // Return results if successful + return results.map((row) => ({ + embedding: this.mapRawToEntity(row), + similarity: parseFloat(row.similarity), + })); + } catch (vectorError) { + console.warn( + 'Vector similarity search failed, falling back to basic filtering:', + vectorError, + ); + + // Fallback to just getting the records by content type + let query = this.repository.createQueryBuilder('vector_embedding'); + + // Add content type filter if provided + if (contentTypes && contentTypes.length > 0) { + query = query + .where('vector_embedding.content_type IN (:...contentTypes)') + .setParameter('contentTypes', contentTypes); + } + + // Limit results + query = query.take(limit); + + // Execute query + const results = await query.getMany(); + + // Return results with a placeholder similarity + return results.map((entity) => ({ + embedding: entity, + similarity: 0.5, // Placeholder similarity + })); + } + } catch (error) { + console.error('Error during vector search:', error); + return []; + } + } + + /** + * Search by text using vector similarity + * @param text Text to search for + * @param getEmbeddingFunc Function to convert text to embedding + * @param limit Maximum number of results + * @param threshold Similarity threshold + * @param contentTypes Optional content types to filter by + */ + async searchByText( + text: string, + getEmbeddingFunc: (text: string) => Promise, + limit = 10, + threshold = 0.7, + contentTypes?: string[], + ): Promise> { + try { + // Get embedding for the search text + const embedding = await getEmbeddingFunc(text); + + // Search by embedding + return this.searchSimilar(embedding, limit, threshold, contentTypes); + } catch (error) { + console.error('Error searching by text:', error); + return []; + } + } + + /** + * Map raw database result to entity + * @param raw Raw database result + */ + private mapRawToEntity(raw: any): VectorEmbedding { + const entity = new VectorEmbedding(); + entity.id = raw.id; + entity.content_type = raw.content_type; + entity.content_id = raw.content_id; + entity.text_content = raw.text_content; + entity.metadata = raw.metadata; + entity.embedding = raw.embedding; + entity.dimensions = raw.dimensions; + entity.model = raw.model; + entity.createdAt = raw.created_at; + entity.updatedAt = raw.updated_at; + return entity; + } + + /** + * Format embedding array for pgvector + * @param embedding Array of embedding values + * @returns Properly formatted vector string for pgvector + */ + private formatEmbeddingForPgVector(embedding: number[] | string): string | null { + if (!embedding) return null; + + // If it's already a string and starts with '[', assume it's formatted + if (typeof embedding === 'string') { + if (embedding.startsWith('[') && embedding.endsWith(']')) { + return embedding; + } + return `[${embedding}]`; + } + + // Format array as proper pgvector string + if (Array.isArray(embedding)) { + return `[${embedding.join(',')}]`; + } + + return null; + } +} + +export default VectorEmbeddingRepository; diff --git a/src/db/repositories/index.ts b/src/db/repositories/index.ts new file mode 100644 index 0000000..b5a277d --- /dev/null +++ b/src/db/repositories/index.ts @@ -0,0 +1,4 @@ +import VectorEmbeddingRepository from './VectorEmbeddingRepository.js'; + +// Export all repositories +export { VectorEmbeddingRepository }; diff --git a/src/db/subscribers/VectorEmbeddingSubscriber.ts b/src/db/subscribers/VectorEmbeddingSubscriber.ts new file mode 100644 index 0000000..19c506a --- /dev/null +++ b/src/db/subscribers/VectorEmbeddingSubscriber.ts @@ -0,0 +1,53 @@ +import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm'; +import { VectorEmbedding } from '../entities/VectorEmbedding.js'; + +/** + * A subscriber to format vector embeddings before saving to database + */ +@EventSubscriber() +export class VectorEmbeddingSubscriber implements EntitySubscriberInterface { + /** + * Indicates that this subscriber only listens to VectorEmbedding events + */ + listenTo() { + return VectorEmbedding; + } + + /** + * Called before entity insertion + */ + beforeInsert(event: InsertEvent) { + this.formatEmbedding(event.entity); + } + + /** + * Called before entity update + */ + beforeUpdate(event: UpdateEvent) { + if (event.entity && event.entity.embedding) { + this.formatEmbedding(event.entity as VectorEmbedding); + } + } + + /** + * Format embedding as a proper vector string + */ + private formatEmbedding(entity: VectorEmbedding | undefined) { + if (!entity || !entity.embedding || !Array.isArray(entity.embedding)) { + return; + } + + // If the embedding is already a string, don't process it + if (typeof entity.embedding === 'string') { + return; + } + + // Format array as proper pgvector string + // Ensure the string starts with '[' and ends with ']' as required by pgvector + const vectorString = `[${entity.embedding.join(',')}]`; + + // Store the string directly (TypeORM will handle conversion) + // We need to use 'as any' because the type is declared as number[] but we're setting a string + (entity as any).embedding = vectorString; + } +} diff --git a/src/db/types/postgresVectorType.ts b/src/db/types/postgresVectorType.ts new file mode 100644 index 0000000..84e6821 --- /dev/null +++ b/src/db/types/postgresVectorType.ts @@ -0,0 +1,38 @@ +import { DataSource } from 'typeorm'; + +/** + * Register the PostgreSQL vector type with TypeORM + * @param dataSource TypeORM data source + */ +export function registerPostgresVectorType(dataSource: DataSource): void { + // Skip if not postgres + if (dataSource.driver.options.type !== 'postgres') { + return; + } + + // Get the postgres driver + const pgDriver = dataSource.driver; + + // Add 'vector' to the list of supported column types + if (pgDriver.supportedDataTypes) { + pgDriver.supportedDataTypes.push('vector' as any); + } + + // Override the normalization for the vector type + if ((pgDriver as any).dataTypeDefaults) { + (pgDriver as any).dataTypeDefaults['vector'] = { + type: 'vector', + }; + } + + // Override the column type resolver to prevent it from converting vector to other types + const originalColumnTypeResolver = (pgDriver as any).columnTypeResolver; + if (originalColumnTypeResolver) { + (pgDriver as any).columnTypeResolver = (column: any) => { + if (column.type === 'vector') { + return 'vector'; + } + return originalColumnTypeResolver(column); + }; + } +} diff --git a/src/index.ts b/src/index.ts index 4c152a7..b29ec8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import AppServer from './server.js'; const appServer = new AppServer(); diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index f7441c8..3f83688 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -9,6 +9,7 @@ import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js'; import config from '../config/index.js'; import { getGroup } from './sseService.js'; import { getServersInGroup } from './groupService.js'; +import { saveToolsAsVectorEmbeddings, searchToolsByVector } from './vectorSearchService.js'; const servers: { [sessionId: string]: Server } = {}; @@ -99,14 +100,21 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => // Add UV_DEFAULT_INDEX from settings if available (for Python packages) const settings = loadSettings(); // Add UV_DEFAULT_INDEX from settings if available (for Python packages) - if (settings.systemConfig?.install?.pythonIndexUrl && conf.command === 'uvx') { + if ( + settings.systemConfig?.install?.pythonIndexUrl && + (conf.command === 'uvx' || conf.command === 'uv' || conf.command === 'python') + ) { env['UV_DEFAULT_INDEX'] = settings.systemConfig.install.pythonIndexUrl; } // Add npm_config_registry from settings if available (for NPM packages) if ( settings.systemConfig?.install?.npmRegistry && - (conf.command === 'npm' || conf.command === 'npx') + (conf.command === 'npm' || + conf.command === 'npx' || + conf.command === 'pnpm' || + conf.command === 'yarn' || + conf.command === 'node') ) { env['npm_config_registry'] = settings.systemConfig.install.npmRegistry; } @@ -168,6 +176,22 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => })); serverInfo.status = 'connected'; serverInfo.error = null; + + // Save tools as vector embeddings for search (only when smart routing is enabled) + if (serverInfo.tools.length > 0) { + try { + const settings = loadSettings(); + const smartRoutingEnabled = settings.systemConfig?.smartRouting?.enabled || false; + if (smartRoutingEnabled) { + console.log( + `Smart routing enabled - saving vector embeddings for server ${name}`, + ); + saveToolsAsVectorEmbeddings(name, serverInfo.tools); + } + } catch (vectorError) { + console.warn(`Failed to save vector embeddings for server ${name}:`, vectorError); + } + } }) .catch((error) => { console.error( @@ -258,7 +282,6 @@ export const addServer = async ( return { success: false, message: 'Failed to save settings' }; } - registerAllTools(false); return { success: true, message: 'Server added successfully' }; } catch (error) { console.error(`Failed to add server: ${name}`, error); @@ -369,6 +392,74 @@ const handleListToolsRequest = async (_: any, extra: any) => { const sessionId = extra.sessionId || ''; const group = getGroup(sessionId); console.log(`Handling ListToolsRequest for group: ${group}`); + + // Special handling for $smart group to return special tools + if (group === '$smart') { + return { + tools: [ + { + name: 'search_tools', + description: (() => { + // Get info about available servers + const availableServers = serverInfos.filter( + (server) => server.status === 'connected' && server.enabled !== false, + ); + // Create simple server information with only server names + const serversList = availableServers + .map((server) => { + return `${server.name}`; + }) + .join(', '); + return `STEP 1 of 2: Use this tool FIRST to discover and search for relevant tools across all available servers. This tool and call_tool work together as a two-step process: 1) search_tools to find what you need, 2) call_tool to execute it. + +For optimal results, use specific queries matching your exact needs. Call this tool multiple times with different queries for different parts of complex tasks. Example queries: "image generation tools", "code review tools", "data analysis", "translation capabilities", etc. Results are sorted by relevance using vector similarity. + +After finding relevant tools, you MUST use the call_tool to actually execute them. The search_tools only finds tools - it doesn't execute them. + +Available servers: ${serversList}`; + })(), + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: + 'The search query to find relevant tools. Be specific and descriptive about the task you want to accomplish.', + }, + limit: { + type: 'integer', + description: + 'Maximum number of results to return. Use higher values (20-30) for broad searches and lower values (5-10) for specific searches.', + default: 10, + }, + }, + required: ['query'], + }, + }, + { + name: 'call_tool', + description: + "STEP 2 of 2: Use this tool AFTER search_tools to actually execute/invoke any tool you found. This is the execution step - search_tools finds tools, call_tool runs them.\n\nWorkflow: search_tools → examine results → call_tool with the chosen tool name and required arguments.\n\nIMPORTANT: Always check the tool's inputSchema from search_tools results before invoking to ensure you provide the correct arguments. The search results will show you exactly what parameters each tool expects.", + inputSchema: { + type: 'object', + properties: { + toolName: { + type: 'string', + description: 'The exact name of the tool to invoke (from search_tools results)', + }, + arguments: { + type: 'object', + description: + 'The arguments to pass to the tool based on its inputSchema (optional if tool requires no arguments)', + }, + }, + required: ['toolName'], + }, + }, + ], + }; + } + const allServerInfos = serverInfos.filter((serverInfo) => { if (serverInfo.enabled === false) return false; if (!group) return true; @@ -392,6 +483,143 @@ const handleListToolsRequest = async (_: any, extra: any) => { const handleCallToolRequest = async (request: any, extra: any) => { console.log(`Handling CallToolRequest for tool: ${request.params.name}`); try { + // Special handling for agent group tools + if (request.params.name === 'search_tools') { + const { query, limit = 10 } = request.params.arguments || {}; + + if (!query || typeof query !== 'string') { + throw new Error('Query parameter is required and must be a string'); + } + + const limitNum = Math.min(Math.max(parseInt(String(limit)) || 10, 1), 100); + + // Dynamically adjust threshold based on query characteristics + let thresholdNum = 0.3; // Default threshold + + // For more general queries, use a lower threshold to get more diverse results + if (query.length < 10 || query.split(' ').length <= 2) { + thresholdNum = 0.2; + } + + // For very specific queries, use a higher threshold for more precise results + if (query.length > 30 || query.includes('specific') || query.includes('exact')) { + thresholdNum = 0.4; + } + + console.log(`Using similarity threshold: ${thresholdNum} for query: "${query}"`); + const servers = undefined; // No server filtering + + const searchResults = await searchToolsByVector(query, limitNum, thresholdNum, servers); + console.log(`Search results: ${JSON.stringify(searchResults)}`); + // Find actual tool information from serverInfos by serverName and toolName + const tools = searchResults.map((result) => { + // Find the server in serverInfos + const server = serverInfos.find( + (serverInfo) => + serverInfo.name === result.serverName && + serverInfo.status === 'connected' && + serverInfo.enabled !== false, + ); + if (server && server.tools && server.tools.length > 0) { + // Find the tool in server.tools + const actualTool = server.tools.find((tool) => tool.name === result.toolName); + if (actualTool) { + // Return the actual tool info from serverInfos + return actualTool; + } + } + + // Fallback to search result if server or tool not found + return { + name: result.toolName, + description: result.description || '', + inputSchema: result.inputSchema || {}, + }; + }); + + // Add usage guidance to the response + const response = { + tools, + metadata: { + query: query, + threshold: thresholdNum, + totalResults: tools.length, + guideline: + tools.length > 0 + ? "Found relevant tools. If these tools don't match exactly what you need, try another search with more specific keywords." + : 'No tools found. Try broadening your search or using different keywords.', + nextSteps: + tools.length > 0 + ? 'To use a tool, call call_tool with the toolName and required arguments.' + : 'Consider searching for related capabilities or more general terms.', + }, + }; + + // Return in the same format as handleListToolsRequest + return { + content: [ + { + type: 'text', + text: JSON.stringify(response), + }, + ], + }; + } + + // Special handling for call_tool + if (request.params.name === 'call_tool') { + const { toolName, arguments: toolArgs = {} } = request.params.arguments || {}; + + if (!toolName) { + throw new Error('toolName parameter is required'); + } + + // arguments parameter is now optional + + let targetServerInfo: ServerInfo | undefined; + + // Find the first server that has this tool + targetServerInfo = serverInfos.find( + (serverInfo) => + serverInfo.status === 'connected' && + serverInfo.enabled !== false && + serverInfo.tools.some((tool) => tool.name === toolName), + ); + + if (!targetServerInfo) { + throw new Error(`No available servers found with tool: ${toolName}`); + } + + // Check if the tool exists on the server + const toolExists = targetServerInfo.tools.some((tool) => tool.name === toolName); + if (!toolExists) { + throw new Error(`Tool '${toolName}' not found on server '${targetServerInfo.name}'`); + } + + // Call the tool on the target server + const client = targetServerInfo.client; + if (!client) { + throw new Error(`Client not found for server: ${targetServerInfo.name}`); + } + + // Use toolArgs if it has properties, otherwise fallback to request.params.arguments + const finalArgs = + toolArgs && Object.keys(toolArgs).length > 0 ? toolArgs : request.params.arguments || {}; + + console.log( + `Invoking tool '${toolName}' on server '${targetServerInfo.name}' with arguments: ${JSON.stringify(finalArgs)}`, + ); + + const result = await client.callTool({ + name: toolName, + arguments: finalArgs, + }); + + console.log(`Tool invocation result: ${JSON.stringify(result)}`); + return result; + } + + // Regular tool handling const serverInfo = getServerByTool(request.params.name); if (!serverInfo) { throw new Error(`Server not found: ${request.params.name}`); diff --git a/src/services/vectorSearchService.ts b/src/services/vectorSearchService.ts new file mode 100644 index 0000000..5bf71a2 --- /dev/null +++ b/src/services/vectorSearchService.ts @@ -0,0 +1,706 @@ +import { getRepositoryFactory } from '../db/index.js'; +import { VectorEmbeddingRepository } from '../db/repositories/index.js'; +import { ToolInfo } from '../types/index.js'; +import { getAppDataSource, initializeDatabase } from '../db/connection.js'; +import { loadSettings } from '../config/index.js'; +import OpenAI from 'openai'; + +// Get OpenAI configuration from smartRouting settings or fallback to environment variables +const getOpenAIConfig = () => { + try { + const settings = loadSettings(); + const smartRouting = settings.systemConfig?.smartRouting; + + return { + apiKey: smartRouting?.openaiApiKey || process.env.OPENAI_API_KEY, + baseURL: + smartRouting?.openaiApiBaseUrl || + process.env.OPENAI_API_BASE_URL || + 'https://api.openai.com/v1', + embeddingModel: + smartRouting?.openaiApiEmbeddingModel || + process.env.OPENAI_API_EMBEDDING_MODEL || + 'text-embedding-3-small', + }; + } catch (error) { + console.warn( + 'Failed to load smartRouting settings, falling back to environment variables:', + error, + ); + return { + apiKey: '', + baseURL: 'https://api.openai.com/v1', + embeddingModel: 'text-embedding-3-small', + }; + } +}; + +// Environment variables for embedding configuration +const EMBEDDING_ENV = { + // The embedding model to use - default to OpenAI but allow BAAI/BGE models + MODEL: process.env.EMBEDDING_MODEL || getOpenAIConfig().embeddingModel, + // Detect if using a BGE model from the environment variable + IS_BGE_MODEL: !!(process.env.EMBEDDING_MODEL && process.env.EMBEDDING_MODEL.includes('bge')), +}; + +// Constants for embedding models +const EMBEDDING_DIMENSIONS = 1536; // OpenAI's text-embedding-3-small outputs 1536 dimensions +const BGE_DIMENSIONS = 1024; // BAAI/bge-m3 outputs 1024 dimensions +const FALLBACK_DIMENSIONS = 100; // Fallback implementation uses 100 dimensions + +// Get dimensions for a model +const getDimensionsForModel = (model: string): number => { + if (model.includes('bge-m3')) { + return BGE_DIMENSIONS; + } else if (model.includes('text-embedding-3')) { + return EMBEDDING_DIMENSIONS; + } else if (model === 'fallback' || model === 'simple-hash') { + return FALLBACK_DIMENSIONS; + } + // Default to OpenAI dimensions + return EMBEDDING_DIMENSIONS; +}; + +// Initialize the OpenAI client with smartRouting configuration +const getOpenAIClient = () => { + const config = getOpenAIConfig(); + return new OpenAI({ + apiKey: config.apiKey, // Get API key from smartRouting settings or environment variables + baseURL: config.baseURL, // Get base URL from smartRouting settings or fallback to default + }); +}; + +/** + * Generate text embedding using OpenAI's embedding model + * + * NOTE: embeddings are 1536 dimensions by default. + * If you previously used the fallback implementation (100 dimensions), + * you may need to rebuild your vector database indices after switching. + * + * @param text Text to generate embeddings for + * @returns Promise with vector embedding as number array + */ +async function generateEmbedding(text: string): Promise { + try { + const config = getOpenAIConfig(); + const openai = getOpenAIClient(); + + // Check if API key is configured + if (!openai.apiKey) { + console.warn('OpenAI API key is not configured. Using fallback embedding method.'); + return generateFallbackEmbedding(text); + } + + // Truncate text if it's too long (OpenAI has token limits) + const truncatedText = text.length > 8000 ? text.substring(0, 8000) : text; + + // Call OpenAI's embeddings API + const response = await openai.embeddings.create({ + model: config.embeddingModel, // Modern model with better performance + input: truncatedText, + }); + + // Return the embedding + return response.data[0].embedding; + } catch (error) { + console.error('Error generating embedding:', error); + console.warn('Falling back to simple embedding method'); + return generateFallbackEmbedding(text); + } +} + +/** + * Fallback embedding function using a simple approach when OpenAI API is unavailable + * @param text Text to generate embeddings for + * @returns Vector embedding as number array + */ +function generateFallbackEmbedding(text: string): number[] { + const words = text.toLowerCase().split(/\s+/); + const vocabulary = [ + 'search', + 'find', + 'get', + 'fetch', + 'retrieve', + 'query', + 'map', + 'location', + 'weather', + 'file', + 'directory', + 'email', + 'message', + 'send', + 'create', + 'update', + 'delete', + 'browser', + 'web', + 'page', + 'click', + 'navigate', + 'screenshot', + 'automation', + 'database', + 'table', + 'record', + 'insert', + 'select', + 'schema', + 'data', + 'image', + 'photo', + 'video', + 'media', + 'upload', + 'download', + 'convert', + 'text', + 'document', + 'pdf', + 'excel', + 'word', + 'format', + 'parse', + 'api', + 'rest', + 'http', + 'request', + 'response', + 'json', + 'xml', + 'time', + 'date', + 'calendar', + 'schedule', + 'reminder', + 'clock', + 'math', + 'calculate', + 'number', + 'sum', + 'average', + 'statistics', + 'user', + 'account', + 'login', + 'auth', + 'permission', + 'role', + ]; + + // Create vector with fallback dimensions + const vector = new Array(FALLBACK_DIMENSIONS).fill(0); + + words.forEach((word) => { + const index = vocabulary.indexOf(word); + if (index >= 0 && index < vector.length) { + vector[index] += 1; + } + // Add some randomness based on word hash + const hash = word.split('').reduce((a, b) => a + b.charCodeAt(0), 0); + vector[hash % vector.length] += 0.1; + }); + + // Normalize the vector + const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0)); + if (magnitude > 0) { + return vector.map((val) => val / magnitude); + } + + return vector; +} + +/** + * Save tool information as vector embeddings + * @param serverName Server name + * @param tools Array of tools to save + */ +export const saveToolsAsVectorEmbeddings = async ( + serverName: string, + tools: ToolInfo[], +): Promise => { + try { + const config = getOpenAIConfig(); + const vectorRepository = getRepositoryFactory( + 'vectorEmbeddings', + )() as VectorEmbeddingRepository; + + for (const tool of tools) { + // Create searchable text from tool information + const searchableText = [ + tool.name, + tool.description, + // Include input schema properties if available + ...(tool.inputSchema && typeof tool.inputSchema === 'object' + ? Object.keys(tool.inputSchema).filter((key) => key !== 'type' && key !== 'properties') + : []), + // Include schema property names if available + ...(tool.inputSchema && + tool.inputSchema.properties && + typeof tool.inputSchema.properties === 'object' + ? Object.keys(tool.inputSchema.properties) + : []), + ] + .filter(Boolean) + .join(' '); + + try { + // Generate embedding + const embedding = await generateEmbedding(searchableText); + + // Check database compatibility before saving + await checkDatabaseVectorDimensions(embedding.length); + + // Save embedding + await vectorRepository.saveEmbedding( + 'tool', + `${serverName}:${tool.name}`, + searchableText, + embedding, + { + serverName, + toolName: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + }, + config.embeddingModel, // Store the model used for this embedding + ); + } catch (toolError) { + console.error(`Error processing tool ${tool.name} for server ${serverName}:`, toolError); + // Continue with the next tool rather than failing the whole batch + } + } + + console.log(`Saved ${tools.length} tool embeddings for server: ${serverName}`); + } catch (error) { + console.error(`Error saving tool embeddings for server ${serverName}:`, error); + } +}; + +/** + * Search for tools using vector similarity + * @param query Search query text + * @param limit Maximum number of results to return + * @param threshold Similarity threshold (0-1) + * @param serverNames Optional array of server names to filter by + */ +export const searchToolsByVector = async ( + query: string, + limit: number = 10, + threshold: number = 0.7, + serverNames?: string[], +): Promise< + Array<{ + serverName: string; + toolName: string; + description: string; + inputSchema: any; + similarity: number; + searchableText: string; + }> +> => { + try { + const vectorRepository = getRepositoryFactory( + 'vectorEmbeddings', + )() as VectorEmbeddingRepository; + + // Search by text using vector similarity + const results = await vectorRepository.searchByText( + query, + generateEmbedding, + limit, + threshold, + ['tool'], + ); + + // Filter by server names if provided + let filteredResults = results; + if (serverNames && serverNames.length > 0) { + filteredResults = results.filter((result) => { + if (typeof result.embedding.metadata === 'string') { + try { + const parsedMetadata = JSON.parse(result.embedding.metadata); + return serverNames.includes(parsedMetadata.serverName); + } catch (error) { + return false; + } + } + return false; + }); + } + + // Transform results to a more useful format + return filteredResults.map((result) => { + // Check if we have metadata as a string that needs to be parsed + if (result.embedding?.metadata && typeof result.embedding.metadata === 'string') { + try { + // Parse the metadata string as JSON + const parsedMetadata = JSON.parse(result.embedding.metadata); + + if (parsedMetadata.serverName && parsedMetadata.toolName) { + // We have properly structured metadata + return { + serverName: parsedMetadata.serverName, + toolName: parsedMetadata.toolName, + description: parsedMetadata.description || '', + inputSchema: parsedMetadata.inputSchema || {}, + similarity: result.similarity, + searchableText: result.embedding.text_content, + }; + } + } catch (error) { + console.error('Error parsing metadata string:', error); + // Fall through to the extraction logic below + } + } + + // Extract tool info from text_content if metadata is not available or parsing failed + const textContent = result.embedding?.text_content || ''; + + // Extract toolName (first word of text_content) + const toolNameMatch = textContent.match(/^(\S+)/); + const toolName = toolNameMatch ? toolNameMatch[1] : ''; + + // Extract serverName from toolName if it follows the pattern "serverName_toolPart" + const serverNameMatch = toolName.match(/^([^_]+)_/); + const serverName = serverNameMatch ? serverNameMatch[1] : 'unknown'; + + // Extract description (everything after the first word) + const description = textContent.replace(/^\S+\s*/, '').trim(); + + return { + serverName, + toolName, + description, + inputSchema: {}, + similarity: result.similarity, + searchableText: textContent, + }; + }); + } catch (error) { + console.error('Error searching tools by vector:', error); + return []; + } +}; + +/** + * Get all available tools in vector database + * @param serverNames Optional array of server names to filter by + */ +export const getAllVectorizedTools = async ( + serverNames?: string[], +): Promise< + Array<{ + serverName: string; + toolName: string; + description: string; + inputSchema: any; + }> +> => { + try { + const config = getOpenAIConfig(); + const vectorRepository = getRepositoryFactory( + 'vectorEmbeddings', + )() as VectorEmbeddingRepository; + + // Try to determine what dimension our database is using + let dimensionsToUse = getDimensionsForModel(config.embeddingModel); // Default based on the model selected + + try { + const result = await getAppDataSource().query(` + SELECT atttypmod as dimensions + FROM pg_attribute + WHERE attrelid = 'vector_embeddings'::regclass + AND attname = 'embedding' + `); + + if (result && result.length > 0 && result[0].dimensions) { + const rawValue = result[0].dimensions; + + if (rawValue === -1) { + // No type modifier specified + dimensionsToUse = getDimensionsForModel(config.embeddingModel); + } else { + // For this version of pgvector, atttypmod stores the dimension value directly + dimensionsToUse = rawValue; + } + } + } catch (error: any) { + console.warn('Could not determine vector dimensions from database:', error?.message); + } + + // Get all tool embeddings + const results = await vectorRepository.searchSimilar( + new Array(dimensionsToUse).fill(0), // Zero vector with dimensions matching the database + 1000, // Large limit + -1, // No threshold (get all) + ['tool'], + ); + + // Filter by server names if provided + let filteredResults = results; + if (serverNames && serverNames.length > 0) { + filteredResults = results.filter((result) => { + if (typeof result.embedding.metadata === 'string') { + try { + const parsedMetadata = JSON.parse(result.embedding.metadata); + return serverNames.includes(parsedMetadata.serverName); + } catch (error) { + return false; + } + } + return false; + }); + } + + // Transform results + return filteredResults.map((result) => { + if (typeof result.embedding.metadata === 'string') { + try { + const parsedMetadata = JSON.parse(result.embedding.metadata); + return { + serverName: parsedMetadata.serverName, + toolName: parsedMetadata.toolName, + description: parsedMetadata.description, + inputSchema: parsedMetadata.inputSchema, + }; + } catch (error) { + console.error('Error parsing metadata string:', error); + return { + serverName: 'unknown', + toolName: 'unknown', + description: '', + inputSchema: {}, + }; + } + } + return { + serverName: 'unknown', + toolName: 'unknown', + description: '', + inputSchema: {}, + }; + }); + } catch (error) { + console.error('Error getting all vectorized tools:', error); + return []; + } +}; + +/** + * Remove tool embeddings for a server + * @param serverName Server name + */ +export const removeServerToolEmbeddings = async (serverName: string): Promise => { + try { + const vectorRepository = getRepositoryFactory( + 'vectorEmbeddings', + )() as VectorEmbeddingRepository; + + // Note: This would require adding a delete method to VectorEmbeddingRepository + // For now, we'll log that this functionality needs to be implemented + console.log(`TODO: Remove tool embeddings for server: ${serverName}`); + } catch (error) { + console.error(`Error removing tool embeddings for server ${serverName}:`, error); + } +}; + +/** + * Sync all server tools embeddings when smart routing is first enabled + * This function will scan all currently connected servers and save their tools as vector embeddings + */ +export const syncAllServerToolsEmbeddings = async (): Promise => { + try { + console.log('Starting synchronization of all server tools embeddings...'); + + // Import getServersInfo to get all server information + const { getServersInfo } = await import('./mcpService.js'); + + const servers = getServersInfo(); + let totalToolsSynced = 0; + let serversSynced = 0; + + for (const server of servers) { + if (server.status === 'connected' && server.tools && server.tools.length > 0) { + try { + console.log(`Syncing tools for server: ${server.name} (${server.tools.length} tools)`); + await saveToolsAsVectorEmbeddings(server.name, server.tools); + totalToolsSynced += server.tools.length; + serversSynced++; + } catch (error) { + console.error(`Failed to sync tools for server ${server.name}:`, error); + } + } else if (server.status === 'connected' && (!server.tools || server.tools.length === 0)) { + console.log(`Server ${server.name} is connected but has no tools to sync`); + } else { + console.log(`Skipping server ${server.name} (status: ${server.status})`); + } + } + + console.log( + `Smart routing tools sync completed: synced ${totalToolsSynced} tools from ${serversSynced} servers`, + ); + } catch (error) { + console.error('Error during smart routing tools synchronization:', error); + throw error; + } +}; + +/** + * Check database vector dimensions and ensure compatibility + * @param dimensionsNeeded The number of dimensions required + * @returns Promise that resolves when check is complete + */ +async function checkDatabaseVectorDimensions(dimensionsNeeded: number): Promise { + try { + // First check if database is initialized + if (!getAppDataSource().isInitialized) { + console.info('Database not initialized, initializing...'); + await initializeDatabase(); + } + + // Check current vector dimension in the database + // First try to get vector type info directly + let vectorTypeInfo; + try { + vectorTypeInfo = await getAppDataSource().query(` + SELECT + atttypmod, + format_type(atttypid, atttypmod) as formatted_type + FROM pg_attribute + WHERE attrelid = 'vector_embeddings'::regclass + AND attname = 'embedding' + `); + } catch (error) { + console.warn('Could not get vector type info, falling back to atttypmod query'); + } + + // Fallback to original query + const result = await getAppDataSource().query(` + SELECT atttypmod as dimensions + FROM pg_attribute + WHERE attrelid = 'vector_embeddings'::regclass + AND attname = 'embedding' + `); + + let currentDimensions = 0; + + // Parse dimensions from result + if (result && result.length > 0 && result[0].dimensions) { + if (vectorTypeInfo && vectorTypeInfo.length > 0) { + // Try to extract dimensions from formatted type like "vector(1024)" + const match = vectorTypeInfo[0].formatted_type?.match(/vector\((\d+)\)/); + if (match) { + currentDimensions = parseInt(match[1]); + } + } + + // If we couldn't extract from formatted type, use the atttypmod value directly + if (currentDimensions === 0) { + const rawValue = result[0].dimensions; + + if (rawValue === -1) { + // No type modifier specified + currentDimensions = 0; + } else { + // For this version of pgvector, atttypmod stores the dimension value directly + currentDimensions = rawValue; + } + } + } + + // Also check the dimensions stored in actual records for validation + try { + const recordCheck = await getAppDataSource().query(` + SELECT dimensions, model, COUNT(*) as count + FROM vector_embeddings + GROUP BY dimensions, model + ORDER BY count DESC + LIMIT 5 + `); + + if (recordCheck && recordCheck.length > 0) { + // If we couldn't determine dimensions from schema, use the most common dimension from records + if (currentDimensions === 0 && recordCheck[0].dimensions) { + currentDimensions = recordCheck[0].dimensions; + } + } + } catch (error) { + console.warn('Could not check dimensions from actual records:', error); + } + + // If no dimensions are set or they don't match what we need, handle the mismatch + if (currentDimensions === 0 || currentDimensions !== dimensionsNeeded) { + console.log( + `Vector dimensions mismatch: database=${currentDimensions}, needed=${dimensionsNeeded}`, + ); + + if (currentDimensions === 0) { + console.log('Setting up vector dimensions for the first time...'); + } else { + console.log('Dimension mismatch detected. Clearing existing incompatible vector data...'); + + // Clear all existing vector embeddings with mismatched dimensions + await clearMismatchedVectorData(dimensionsNeeded); + } + + // Drop any existing indices first + await getAppDataSource().query(`DROP INDEX IF EXISTS idx_vector_embeddings_embedding;`); + + // Alter the column type with the new dimensions + await getAppDataSource().query(` + ALTER TABLE vector_embeddings + ALTER COLUMN embedding TYPE vector(${dimensionsNeeded}); + `); + + // Create a new index with better error handling + try { + await getAppDataSource().query(` + CREATE INDEX idx_vector_embeddings_embedding + ON vector_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + `); + } catch (indexError: any) { + // If the index already exists (code 42P07) or there's a duplicate key constraint (code 23505), + // it's not a critical error as the index is already there + if (indexError.code === '42P07' || indexError.code === '23505') { + console.log('Index already exists, continuing...'); + } else { + console.warn('Warning: Failed to create index, but continuing:', indexError.message); + } + } + + console.log(`Successfully configured vector dimensions to ${dimensionsNeeded}`); + } + } catch (error: any) { + console.error('Error checking/updating vector dimensions:', error); + throw new Error(`Vector dimension check failed: ${error?.message || 'Unknown error'}`); + } +} + +/** + * Clear vector embeddings with mismatched dimensions + * @param expectedDimensions The expected dimensions + * @returns Promise that resolves when cleanup is complete + */ +async function clearMismatchedVectorData(expectedDimensions: number): Promise { + try { + console.log( + `Clearing vector embeddings with dimensions different from ${expectedDimensions}...`, + ); + + // Delete all embeddings that don't match the expected dimensions + await getAppDataSource().query( + ` + DELETE FROM vector_embeddings + WHERE dimensions != $1 + `, + [expectedDimensions], + ); + + console.log('Successfully cleared mismatched vector embeddings'); + } catch (error: any) { + console.error('Error clearing mismatched vector data:', error); + throw error; + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 3c4c231..a66d0ac 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -90,6 +90,13 @@ export interface McpSettings { pythonIndexUrl?: string; // Python package repository URL (UV_DEFAULT_INDEX) npmRegistry?: string; // NPM registry URL (npm_config_registry) }; + smartRouting?: { + enabled?: boolean; // Controls whether smart routing is enabled + dbUrl?: string; // Database URL for smart routing + openaiApiBaseUrl?: string; // OpenAI API base URL + openaiApiKey?: string; // OpenAI API key + openaiApiEmbeddingModel?: string; // OpenAI API embedding model + }; // Add other system configuration sections here in the future }; } diff --git a/tsconfig.json b/tsconfig.json index 79a2759..227cf73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,10 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "sourceMap": true + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strictPropertyInitialization": false }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.test.ts", "dist"]