diff --git a/app/.gitignore b/app/.gitignore index 79903bc0b8d..28bf77de052 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -13,3 +13,11 @@ electron/windowSize.test.js electron/env-paths.js electron/runCmd.test.js +electron/mcp/MCPClient.js +electron/mcp/MCPClient.test.js +electron/mcp/MCPToolStateStore.js +electron/mcp/MCPToolStateStore.test.js +electron/settings.js +electron/settings.test.js +electron/mcp/MCPSettings.js +electron/mcp/MCPSettings.test.js diff --git a/app/electron/main.ts b/app/electron/main.ts index 672a678ca2c..286151cc1af 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -41,6 +41,7 @@ import url from 'url'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import i18n from './i18next.config'; +import MCPClient from './mcp/MCPClient'; import { addToPath, ArtifactHubHeadlampPkg, @@ -58,6 +59,9 @@ if (process.env.HEADLAMP_RUN_SCRIPT) { runScript(); } +// Enabled by default, set HEADLAMP_MCP_ENABLE=false to disable MCP features +const ENABLE_MCP = process.env.HEADLAMP_MCP_ENABLE !== 'false'; + dotenv.config({ path: path.join(process.resourcesPath, '.env') }); const isDev = process.env.ELECTRON_DEV || false; @@ -139,6 +143,7 @@ const shouldCheckForUpdates = process.env.HEADLAMP_CHECK_FOR_UPDATES !== 'false' // make it global so that it doesn't get garbage collected let mainWindow: BrowserWindow | null; +let mcpClient: MCPClient | null = null; /** * `Action` is an interface for an action to be performed by the plugin manager. @@ -1602,6 +1607,14 @@ function startElecron() { if (userPluginBinDirs.length > 0) { addToPath(userPluginBinDirs, 'userPluginBinDirs plugin'); } + + if (ENABLE_MCP) { + const configPath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); + const settingsPath = path.join(app.getPath('userData'), 'mcp-tools-settings.json'); + mcpClient = new MCPClient(configPath, settingsPath); + await mcpClient.initialize(); + mcpClient.setMainWindow(mainWindow); + } } if (disableGPU) { @@ -1632,12 +1645,21 @@ function startElecron() { app.once('window-all-closed', app.quit); - app.once('before-quit', () => { + app.once('before-quit', async () => { saveZoomFactor(cachedZoom); i18n.off('languageChanged'); if (mainWindow) { mainWindow.removeAllListeners('close'); } + + if (mcpClient) { + try { + await mcpClient.cleanup(); + mcpClient = null; + } catch (err) { + console.error('Failed to clean up mcpClient:', err); + } + } }); } diff --git a/app/electron/mcp/MCPClient.test.ts b/app/electron/mcp/MCPClient.test.ts new file mode 100644 index 00000000000..71750fd64c6 --- /dev/null +++ b/app/electron/mcp/MCPClient.test.ts @@ -0,0 +1,466 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import MCPClient from './MCPClient'; + +function tmpPath(): string { + return path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); +} + +describe('MCPClient', () => { + let client: MCPClient; + let infoSpy: jest.Mock; + + let cfgPath: string; + let settingsPath: string; + + beforeEach(() => { + cfgPath = tmpPath(); + settingsPath = tmpPath(); + try { + if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); + } catch { + // ignore + } + try { + if (fs.existsSync(settingsPath)) fs.unlinkSync(settingsPath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); + } catch { + // ignore + } + try { + if (fs.existsSync(settingsPath)) fs.unlinkSync(settingsPath); + } catch { + // ignore + } + }); + + beforeEach(() => { + client = new MCPClient(cfgPath, settingsPath); + // spy on console.info to avoid noisy output and to assert calls + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}) as unknown as jest.Mock; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('throws from handleClustersChange if not initialized', async () => { + await expect(client.handleClustersChange(['cluster-a'])).rejects.toThrow( + 'MCPClient: not initialized' + ); + }); + + it('initialize is idempotent and logs exactly once', async () => { + await client.initialize(); + await client.initialize(); // second call should be a no-op + + expect(infoSpy).toHaveBeenCalledTimes(1); + expect(infoSpy).toHaveBeenCalledWith('MCPClient: initialized'); + }); + + it('config is set after initialize', async () => { + expect((client as any).mcpToolState).toBeNull(); + await client.initialize(); + expect((client as any).mcpToolState).not.toBeNull(); + }); + + it('handleClustersChange resolves when initialized and logs clusters', async () => { + await client.initialize(); + await expect(client.handleClustersChange(['cluster-1'])).resolves.toBeUndefined(); + + // initialize + clusters change => at least two calls + expect(infoSpy).toHaveBeenCalledWith('MCPClient: clusters changed ->', ['cluster-1']); + }); + + it('setMainWindow accepts a BrowserWindow-like object and cleanup resets state', async () => { + // use a minimal fake to represent BrowserWindow + const fakeWin = { id: 42 } as unknown as Electron.BrowserWindow; + + await client.initialize(); + client.setMainWindow(fakeWin); + + // handleClustersChange should work when initialized + await expect(client.handleClustersChange(['c-x'])).resolves.toBeUndefined(); + + // cleanup should reset initialized and log cleanup + await client.cleanup(); + expect(infoSpy).toHaveBeenCalledWith('MCPClient: cleaned up'); + + // after cleanup, handleClustersChange should again reject as not initialized + await expect(client.handleClustersChange(['after-cleanup'])).rejects.toThrow( + 'MCPClient: not initialized' + ); + }); + + it('cleanup is safe to call when not initialized', async () => { + // no initialize called + await expect(client.cleanup()).resolves.toBeUndefined(); + // no cleanup log should be emitted since it early-returns when not initialized + expect(infoSpy).not.toHaveBeenCalledWith('MCPClient: cleaned up'); + }); + + it('initialize marks isInitialized and leaves client null when no servers are configured', async () => { + // Mock MCPSettings to return no servers + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings: jest.fn().mockReturnValue({}), + hasClusterDependentServers: jest.fn().mockReturnValue(false), + })); + // Mock MultiServerMCPClient just in case, it should not be constructed + const MultiServerMCPClientMock = jest.fn(); + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + await client.initialize(); + + expect((client as any).isInitialized).toBe(true); + expect((client as any).client).toBeNull(); + // ensure the public log happened + expect(infoSpy).toHaveBeenCalledWith('MCPClient: initialized'); + }); + + it('initialize constructs MCP client and caches tools when servers exist', async () => { + const fakeTools = [{ name: 't1' }, { name: 't2' }]; + const getTools = jest.fn().mockResolvedValue(fakeTools); + const close = jest.fn().mockResolvedValue(undefined); + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => ({ getTools, close })); + + jest.resetModules(); + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings: jest.fn().mockReturnValue({ serverA: { url: 'http://x' } }), + hasClusterDependentServers: jest.fn().mockReturnValue(false), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + await client.initialize(); + + // Ensure the mock constructor was called to create the client + expect(MultiServerMCPClientMock).toHaveBeenCalled(); + // Ensure tools were cached + expect((client as any).clientTools).toEqual(fakeTools); + // Ensure MCPToolStateStore was initialized (non-null) + expect((client as any).mcpToolState).not.toBeNull(); + }); + + it('handleClustersChange logs and returns early when no cluster-dependent servers', async () => { + const getTools = jest.fn().mockResolvedValue([]); + const close = jest.fn().mockResolvedValue(undefined); + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => ({ getTools, close })); + + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + // make servers exist, but indicate no cluster-dependent servers + const makeMcpServersFromSettings = jest.fn().mockReturnValue({ serverA: {} }); + const hasClusterDependentServers = jest.fn().mockReturnValue(false); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings, + hasClusterDependentServers, + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + await client.initialize(); + + const beforeClient = (client as any).client; + + await client.handleClustersChange(['cluster-x']); + + // since hasClusterDependentServers returned false, client unchanged + expect((client as any).client).toBe(beforeClient); + }); + + it('handleClustersChange does nothing when clusters array is identical', async () => { + const getTools = jest.fn().mockResolvedValue([]); + const close = jest.fn().mockResolvedValue(undefined); + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => ({ getTools, close })); + + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings: jest.fn().mockReturnValue({ serverA: {} }), + hasClusterDependentServers: jest.fn().mockReturnValue(true), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + await client.initialize(); + + // set currentClusters to a value and call with the same value + (client as any).currentClusters = ['same-cluster']; + + jest.spyOn(console, 'info').mockImplementation(() => {}); + const closeSpy = jest.spyOn((client as any).client, 'close').mockImplementation(async () => {}); + + await client.handleClustersChange(['same-cluster']); + + // close should not have been called because clusters didn't change + expect(closeSpy).not.toHaveBeenCalled(); + }); + + it('handleClustersChange restarts client when cluster-dependent servers exist', async () => { + const getToolsFirst = jest.fn().mockResolvedValue([{ name: 'a' }]); + const closeFirst = jest.fn().mockResolvedValue(undefined); + const getToolsSecond = jest.fn().mockResolvedValue([{ name: 'b' }]); + const closeSecond = jest.fn().mockResolvedValue(undefined); + + // We'll create a factory to return different instances on subsequent constructions + const instances: any[] = [ + { getTools: getToolsFirst, close: closeFirst }, + { getTools: getToolsSecond, close: closeSecond }, + ]; + const MultiServerMCPClientMock = jest.fn().mockImplementation(() => instances.shift()); + + // Ensure module cache is cleared so our doMock is respected when requiring the MCPClient module + jest.resetModules(); + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: MultiServerMCPClientMock, + })); + const makeMcpServersFromSettings = jest.fn().mockReturnValue({ serverA: {} }); + const hasClusterDependentServers = jest.fn().mockReturnValue(true); + jest.doMock('./MCPSettings', () => ({ + makeMcpServersFromSettings, + hasClusterDependentServers, + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath); + + jest.spyOn(console, 'log').mockImplementation(() => {}); + await client.initialize(); + + // initial client should be the first instance + const firstClientRef = (client as any).client; + expect(firstClientRef).not.toBeNull(); + + // Trigger cluster change: should create a new client for the new cluster + await client.handleClustersChange(['new-cluster']); + + // The MCP client constructor should have been called for initial setup and again for restart + expect(MultiServerMCPClientMock).toHaveBeenCalledTimes(2); + + // After restart, client should have been replaced + const afterClientRef = (client as any).client; + expect(afterClientRef).not.toBeNull(); + // And tools should reflect second initialization + expect((client as any).clientTools).toEqual([{ name: 'b' }]); + }); +}); + +describe('MCPClient logging behavior', () => { + it('logs clusters change even when not initialized', async () => { + const cfgPath = path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); + const client = new (require('./MCPClient').default)(cfgPath) as InstanceType< + typeof import('./MCPClient').default + >; + + const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}) as jest.Mock; + + await expect(client.handleClustersChange(['cluster-log'])).rejects.toThrow( + 'MCPClient: not initialized' + ); + + expect(infoSpy).toHaveBeenCalledWith('MCPClient: clusters changed ->', ['cluster-log']); + + infoSpy.mockRestore(); + }); +}); + +describe('MCPClient#mcpExecuteTool', () => { + const cfgPath = tmpPath(); + const settingsPath = tmpPath(); + + beforeEach(() => { + try { + if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); + } catch {} + try { + if (fs.existsSync(settingsPath)) fs.unlinkSync(settingsPath); + } catch {} + }); + + it('executes a tool successfully and records usage', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest.fn().mockImplementation((fullName: string) => { + const [serverName, ...rest] = fullName.split('.'); + return { serverName, toolName: rest.join('.') }; + }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({ + // initialize config from client tools is invoked during MCPClient.initialize + // provide a no-op mock so tests that don't assert this behavior don't fail + initConfigFromClientTools: jest.fn(), + })), + })); + + // Ensure initialize can construct a client with getTools/close methods + jest.doMock('@langchain/mcp-adapters', () => ({ + MultiServerMCPClient: jest.fn().mockImplementation(() => ({ + getTools: jest.fn().mockResolvedValue([]), + close: jest.fn().mockResolvedValue(undefined), + })), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath) as any; + + await client.initialize(); + + const invoke = jest.fn().mockResolvedValue({ ok: true }); + client.clientTools = [{ name: 'serverA.tool1', schema: {}, invoke }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(true), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + client.client = {}; + + const res = await client.mcpExecuteTool('serverA.tool1', [{ a: 1 }], 'call-1'); + + expect(res.success).toBe(true); + expect(res.result).toEqual({ ok: true }); + expect(res.toolCallId).toBe('call-1'); + expect(client.mcpToolState.recordToolUsage).toHaveBeenCalledWith('serverA', 'tool1'); + }); + + it('returns error when parameter validation fails', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest + .fn() + .mockReturnValue({ serverName: 'serverA', toolName: 'tool1' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: false, error: 'bad-params' }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({ + initConfigFromClientTools: jest.fn(), + })), + })); + + const MCPClient = require('./MCPClient').default as typeof import('./MCPClient').default; + const client = new MCPClient(cfgPath, settingsPath) as any; + + // ensure the client is initialized so mcpExecuteTool follows the normal execution path + await client.initialize(); + + client.clientTools = [{ name: 'serverA.tool1', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(true), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + // provide a minimal client object so mcpExecuteTool does not early-return + client.client = {}; + + const res = await client.mcpExecuteTool('serverA.tool1', [], 'call-2'); + expect(res.success).toBe(false); + expect(res.error).toMatch(/Parameter validation failed: bad-params/); + expect(res.toolCallId).toBe('call-2'); + }); + + it('returns error when tool is disabled', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest.fn().mockReturnValue({ serverName: 's', toolName: 't' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({})), + })); + + const client = new MCPClient(cfgPath, settingsPath) as any; + + client.clientTools = [{ name: 's.t', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(false), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + client.client = {}; + + const res = await client.mcpExecuteTool('s.t', [], 'call-3'); + expect(res.success).toBe(false); + expect(res.error).toMatch(/disabled/); + expect(res.toolCallId).toBe('call-3'); + }); + + it('returns error when tool not found', async () => { + jest.resetModules(); + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest + .fn() + .mockReturnValue({ serverName: 'srv', toolName: 'missing' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({})), + })); + + const client = new MCPClient(cfgPath, settingsPath) as any; + + // clientTools does not contain the requested tool + client.clientTools = [{ name: 'srv.other', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = { + isToolEnabled: jest.fn().mockReturnValue(true), + recordToolUsage: jest.fn(), + }; + client.isInitialized = true; + // provide a minimal client object so mcpExecuteTool does not early-return + client.client = {}; + + const res = await client.mcpExecuteTool('srv.missing', [], 'call-4'); + expect(res.success).toBe(false); + expect(res.error).toMatch(/not found/); + expect(res.toolCallId).toBe('call-4'); + }); + + it('returns undefined when mcpToolState is not set', async () => { + jest.resetModules(); + // Keep default behavior for parse/validate but it's irrelevant here + jest.doMock('./MCPToolStateStore', () => ({ + parseServerNameToolName: jest.fn().mockReturnValue({ serverName: 'x', toolName: 'y' }), + validateToolArgs: jest.fn().mockReturnValue({ valid: true }), + MCPToolStateStore: jest.fn().mockImplementation(() => ({})), + })); + + const client = new MCPClient(cfgPath, settingsPath) as any; + + client.clientTools = [{ name: 'x.y', schema: {}, invoke: jest.fn() }]; + client.mcpToolState = null; + client.isInitialized = true; + + const res = await client.mcpExecuteTool('x.y', [], 'call-5'); + expect(res).toBeUndefined(); + }); +}); diff --git a/app/electron/mcp/MCPClient.ts b/app/electron/mcp/MCPClient.ts new file mode 100644 index 00000000000..ad1d80ede5d --- /dev/null +++ b/app/electron/mcp/MCPClient.ts @@ -0,0 +1,593 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DynamicStructuredTool } from '@langchain/core/dist/tools/index'; +import { MultiServerMCPClient } from '@langchain/mcp-adapters'; +import { type BrowserWindow, dialog, ipcMain } from 'electron'; +import { + hasClusterDependentServers, + loadMCPSettings, + makeMcpServersFromSettings, + MCPSettings, + saveMCPSettings, + showSettingsChangeDialog, +} from './MCPSettings'; +import { + MCPToolsConfig, + MCPToolStateStore, + parseServerNameToolName, + showToolsConfigConfirmationDialog, + validateToolArgs, +} from './MCPToolStateStore'; + +const DEBUG = true; + +/** + * MCPClient + * + * Lightweight client intended for use in the Electron main process to manage + * minimal MCP (Multi-Cluster Platform) concerns. + * + * Example: + * ```ts + * const configPath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); + * const mainWindow = new BrowserWindow({ ... }); + * const settingsPath = path.join(app.getPath('userData'), 'settings.json'); + * const mcpClient = new MCPClient(configPath, settingsPath); + * await mcpClient.initialize(); + * mcpClient.setMainWindow(mainWindow); + * await mcpClient.handleClustersChange(['cluster-1']); + * await mcpClient.cleanup(); + * ``` + */ +export default class MCPClient { + private mainWindow: BrowserWindow | null = null; + private initialized = false; + private mcpToolState: MCPToolStateStore | null = null; + private readonly configPath: string; + + /** Cached list of available tools from all MCP servers */ + private clientTools: DynamicStructuredTool[] = []; + /** The LangChain MCP client instance managing multiple servers */ + private client: MultiServerMCPClient | null = null; + /** Whether the MCP client has been successfully initialized */ + private isInitialized = false; + /** Promise tracking ongoing initialization to prevent duplicate initializations */ + private initializationPromise: Promise | null = null; + + private settingsPath: string; + private clusters: string[] = []; + + private currentClusters: string[] | null = null; + private oldClusters: string[] | null = null; + + constructor(configPath: string, settingsPath: string) { + this.configPath = configPath; + this.settingsPath = settingsPath; + this.setupIpcHandlers(); + } + + /** + * Initialize the MCP client. + */ + async initialize(): Promise { + if (this.initialized) { + return; + } + this.mcpToolState = new MCPToolStateStore(this.configPath); + + await this.initializeClient(); + + this.initialized = true; + + if (DEBUG) { + console.info('MCPClient: initialized'); + } + } + + /** + * Initialize the MCP client if not already initialized. + * + * @return Promise that resolves when initialization is complete. + */ + private async initializeClient(): Promise { + if (DEBUG) { + console.log('MCPClient: initializeClient: ', { + isInitialized: this.isInitialized, + initializationPromise: this.initializationPromise, + }); + } + + if (this.isInitialized) { + return; + } + if (this.initializationPromise) { + return this.initializationPromise; + } + + if (DEBUG) { + console.log('MCPClient: initializeClient: Starting doInitialize()...'); + } + + this.initializationPromise = this.doInitializeClient(); + return this.initializationPromise; + } + + /** + * Perform the actual initialization of the MCP client. + * + * @throws {Error} If initialization fails + */ + private async doInitializeClient(): Promise { + try { + const mcpServers = makeMcpServersFromSettings(this.settingsPath, this.clusters); + + // If no enabled servers, skip initialization + if (Object.keys(mcpServers).length === 0) { + if (DEBUG) { + console.log('MCPClient: doInitialize: No enabled MCP servers found'); + } + this.isInitialized = true; + return; + } + if (DEBUG) { + console.log( + 'MCPClient: doInitialize: Initializing MCP client with servers:', + Object.keys(mcpServers) + ); + } + this.client = new MultiServerMCPClient({ + throwOnLoadError: false, // Don't throw on load error to allow partial initialization + prefixToolNameWithServerName: true, // Prefix to avoid name conflicts + additionalToolNamePrefix: '', + useStandardContentBlocks: true, + mcpServers, + defaultToolTimeout: 2 * 60 * 1000, // 2 minutes + }); + // Get and cache the tools + this.clientTools = await this.client.getTools(); + this.mcpToolState?.initConfigFromClientTools(this.clientTools); + + this.isInitialized = true; + if (DEBUG) { + console.log( + 'MCPClient: doInitialize: MCP client initialized successfully with', + this.clientTools.length, + 'tools' + ); + } + } catch (error) { + console.error('Failed to initialize MCP client:', error); + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + throw error; + } + } + + /** + * Clean up resources used by the MCP client. + */ + async cleanup(): Promise { + if (!this.initialized) { + return; + } + this.mainWindow = null; + this.initialized = false; + + if (this.client) { + try { + await this.client.close(); + } catch (error) { + console.error('Error cleaning up MCP client:', error); + } + } + this.client = null; + this.clientTools = []; + this.isInitialized = false; + this.initializationPromise = null; + + if (DEBUG) { + console.info('MCPClient: cleaned up'); + } + } + + /** + * Set the main BrowserWindow for IPC notifications. + * + * @param win - The main BrowserWindow instance, or null to clear it. + */ + setMainWindow(win: BrowserWindow | null): void { + this.mainWindow = win; + } + + /** + * Handle clusters change notification. + * + * @param clusters - The new active clusters array, or null if none. + */ + async handleClustersChange(newClusters: string[] | null): Promise { + if (DEBUG) { + console.info('MCPClient: clusters changed ->', newClusters); + } + + if (!this.initialized) { + throw new Error('MCPClient: not initialized'); + } + + // If cluster hasn't actually changed, do nothing. + if (JSON.stringify(this.currentClusters) === JSON.stringify(newClusters)) { + return; + } + + const oldClusters = this.currentClusters; + this.currentClusters = newClusters; + + // Check if we have any cluster-dependent servers + if (!hasClusterDependentServers(this.settingsPath)) { + console.log('No cluster-dependent MCP servers found, skipping restart'); + return; + } + + try { + // Reset the client + if (this.client) { + if (typeof (this.client as any).close === 'function') { + await (this.client as any).close(); + } + } + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + // Re-initialize with new cluster context + await this.initializeClient(); + console.log('MCP client restarted successfully for new cluster:', newClusters); + } catch (error) { + console.error('Error restarting MCP client for cluster change:', error); + // Restore previous cluster on error + this.currentClusters = oldClusters; + throw error; + } + } + + /** + * Execute an MCP tool with given parameters. + * + * @param toolName - The full name of the tool to execute (including server prefix) + * @param args - The arguments to pass to the tool + * @param toolCallId - Unique identifier for this tool call + * + * @returns Result object containing success status and output or error message + */ + private async mcpExecuteTool(toolName: string, args: any[], toolCallId: string) { + console.log('args in mcp-execute-tool:', args); + if (!this.mcpToolState) { + return; + } + try { + await this.initializeClient(); + if (!this.client || this.clientTools.length === 0) { + throw new Error('MCP client not initialized or no tools available'); + } + // Parse tool name + const { serverName, toolName: actualToolName } = parseServerNameToolName(toolName); + + // Check if tool is enabled + const isEnabled = this.mcpToolState.isToolEnabled(serverName, actualToolName); + if (!isEnabled) { + throw new Error(`Tool ${actualToolName} from server ${serverName} is disabled`); + } + // Find the tool by name + const tool = this.clientTools.find(t => t.name === toolName); + if (!tool) { + throw new Error(`Tool ${toolName} not found`); + } + // Validate parameters against schema from configuration + const validation = validateToolArgs(tool.schema, args); + + if (!validation.valid) { + throw new Error(`Parameter validation failed: ${validation.error}`); + } + console.log(`Executing MCP tool: ${toolName} with args:`, args); + // Execute the tool directly using LangChain's invoke method + const result = await tool.invoke(args); + console.log(`MCP tool ${toolName} executed successfully`); + // Record tool usage + this.mcpToolState.recordToolUsage(serverName, actualToolName); + return { + success: true, + result, + toolCallId, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + toolCallId, + }; + } + } + + private async mcpGetStatus() { + return { + isInitialized: this.isInitialized, + hasClient: this.client !== null, + }; + } + + private async mcpResetClient() { + try { + if (!this.mainWindow) { + throw new Error('Main window not set for MCP client'); + } + // Show confirmation dialog + const userConfirmed = await showConfirmationDialog( + this.mainWindow, + 'MCP Client Reset', + 'The application wants to reset the MCP client. This will restart all MCP server connections.', + 'Reset MCP client' + ); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the operation', + }; + } + + console.log('Resetting MCP client...'); + + if (this.client) { + // If the client has a close/dispose method, call it + if (typeof (this.client as any).close === 'function') { + await (this.client as any).close(); + } + } + + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + + // Re-initialize + await this.initializeClient(); + + return { success: true }; + } catch (error) { + console.error('Error resetting MCP client:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpUpdateConfig(mcpSettings: MCPSettings) { + try { + if (!this.mainWindow) { + throw new Error('Main window not set for MCP client'); + } + // Get current configuration for comparison + const currentSettings = loadMCPSettings(this.settingsPath); + console.log('Requested MCP configuration update:', mcpSettings); + // Show detailed confirmation dialog with changes + const userConfirmed = await showSettingsChangeDialog( + this.mainWindow, + currentSettings, + mcpSettings + ); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the configuration update', + }; + } + + console.log('Updating MCP configuration with user confirmation...'); + saveMCPSettings(this.settingsPath, mcpSettings); + + // Reset and reinitialize client with new config + if (this.client && typeof this.client.close === 'function') { + await this.client.close(); + } + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + + // Re-initialize with new config + await this.initializeClient(); + + console.log('MCP configuration updated successfully'); + return { success: true }; + } catch (error) { + console.error('Error updating MCP configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpGetConfig() { + try { + const currentSettings = loadMCPSettings(this.settingsPath); + + return { + success: true, + config: currentSettings || { enabled: false, servers: [] }, + }; + } catch (error) { + console.error('Error getting MCP configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + config: { enabled: false, servers: [] }, + }; + } + } + + private async mcpGetToolsConfig() { + try { + const toolsConfig = this.mcpToolState?.getConfig(); + return { + success: true, + config: toolsConfig, + }; + } catch (error) { + console.error('Error getting MCP tools configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + config: {}, + }; + } + } + + private async mcpUpdateToolsConfig(toolsConfig: MCPToolsConfig) { + console.log('Requested MCP tools configuration update:', toolsConfig); + try { + if (!this.mainWindow) { + throw new Error('Main window not set for MCP client'); + } + // Show confirmation dialog with detailed changes + const currentToolsConfig = this.mcpToolState?.getConfig() || {}; + const userConfirmed = await showToolsConfigConfirmationDialog( + this.mainWindow, + currentToolsConfig, + toolsConfig + ); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the operation', + }; + } + + this.mcpToolState?.setConfig(toolsConfig); + return { success: true }; + } catch (error) { + console.error('Error updating MCP tools configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpSetToolEnabled(serverName: string, toolName: string, enabled: boolean) { + try { + this.mcpToolState?.setToolEnabled(serverName, toolName, enabled); + return { success: true }; + } catch (error) { + console.error('Error setting tool enabled state:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async mcpGetToolStats(serverName: string, toolName: string) { + try { + const stats = this.mcpToolState?.getToolStats(serverName, toolName); + return { + success: true, + stats, + }; + } catch (error) { + console.error('Error getting tool statistics:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + stats: null, + }; + } + } + + private async mcpClusterChange(cluster: string | null) { + try { + console.log('Received cluster change event:', cluster); + if (cluster !== null) { + // @todo: support multiple clusters + await this.handleClustersChange([cluster]); + } + return { + success: true, + }; + } catch (error) { + console.error('Error handling cluster change:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + /** + * Setup IPC handlers for MCP operations. + */ + private setupIpcHandlers(): void { + ipcMain?.handle('mcp-execute-tool', async (event, { toolName, args, toolCallId }) => + this.mcpExecuteTool(toolName, args, toolCallId) + ); + ipcMain?.handle('mcp-get-status', async () => this.mcpGetStatus()); + ipcMain?.handle('mcp-reset-client', async () => this.mcpResetClient()); + ipcMain?.handle('mcp-update-config', async (event, mcpSettings: MCPSettings) => + this.mcpUpdateConfig(mcpSettings) + ); + ipcMain?.handle('mcp-get-config', async () => this.mcpGetConfig()); + ipcMain?.handle('mcp-get-tools-config', async () => this.mcpGetToolsConfig()); + ipcMain?.handle('mcp-update-tools-config', async (event, toolsConfig: MCPToolsConfig) => + this.mcpUpdateToolsConfig(toolsConfig) + ); + ipcMain?.handle('mcp-set-tool-enabled', async (event, { serverName, toolName, enabled }) => + this.mcpSetToolEnabled(serverName, toolName, enabled) + ); + ipcMain?.handle('mcp-get-tool-stats', async (event, { serverName, toolName }) => + this.mcpGetToolStats(serverName, toolName) + ); + ipcMain?.handle('mcp-cluster-change', async (event, { cluster }) => + this.mcpClusterChange(cluster) + ); + } +} + +/** + * Show user confirmation dialog for MCP operations. + * Displays a dialog to the user for security confirmation before executing MCP operations. + * + * @param title - Dialog title + * @param message - Main message to display to the user + * @param operation - Description of the operation being performed + * @returns Promise resolving to true if user allows the operation, false otherwise + */ +export async function showConfirmationDialog( + mainWindow: BrowserWindow, + title: string, + message: string, + operation: string +): Promise { + const result = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Allow', 'Cancel'], + defaultId: 1, + title, + message, + detail: `Operation: ${operation}\n\nDo you want to allow this MCP operation?`, + }); + return result.response === 0; // 0 is "Allow" +} diff --git a/app/electron/mcp/MCPSettings.test.ts b/app/electron/mcp/MCPSettings.test.ts new file mode 100644 index 00000000000..9bb53799810 --- /dev/null +++ b/app/electron/mcp/MCPSettings.test.ts @@ -0,0 +1,335 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loadSettings, saveSettings } from '../settings'; +import { expandEnvAndResolvePaths, loadMCPSettings, saveMCPSettings } from './MCPSettings'; +import * as MCP from './MCPSettings'; + +jest.mock('../settings', () => ({ + loadSettings: jest.fn(), + saveSettings: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('MCPSettings', () => { + it('loadMCPSettings returns mcp settings when present', () => { + const expected = { + enabled: true, + servers: [{ name: 's1', command: 'cmd', args: ['-v'], enabled: true }], + }; + (loadSettings as jest.Mock).mockReturnValue({ mcp: expected }); + + const result = loadMCPSettings('/path/to/settings.json'); + + expect(loadSettings).toHaveBeenCalledWith('/path/to/settings.json'); + expect(result).toEqual(expected); + }); + + it('loadMCPSettings returns null when no mcp settings', () => { + (loadSettings as jest.Mock).mockReturnValue({ other: 123 }); + + const result = loadMCPSettings('/settings'); + + expect(loadSettings).toHaveBeenCalledWith('/settings'); + expect(result).toBeNull(); + }); + + it('saveMCPSettings sets mcp on loaded settings and calls saveSettings', () => { + const existing = { someKey: 'value' }; + (loadSettings as jest.Mock).mockReturnValue(existing); + + const newMCP = { + enabled: false, + servers: [{ name: 's', command: 'c', args: [], enabled: false }], + }; + + saveMCPSettings('/cfg', newMCP); + + expect(loadSettings).toHaveBeenCalledWith('/cfg'); + expect((existing as any).mcp).toBe(newMCP); + expect(saveSettings).toHaveBeenCalledWith('/cfg', existing); + }); +}); + +describe('expandEnvAndResolvePaths', () => { + beforeEach(() => { + // Ensure predictable environment vars + process.env.APPDATA = process.env.APPDATA || ''; + process.env.LOCALAPPDATA = process.env.LOCALAPPDATA || ''; + }); + + it('replaces HEADLAMP_CURRENT_CLUSTER with cluster', () => { + const result = expandEnvAndResolvePaths(['connect HEADLAMP_CURRENT_CLUSTER'], 'my-current'); + expect(result).toEqual(['connect my-current']); + }); + + it('replaces %APPDATA% and %LOCALAPPDATA% with environment values', () => { + process.env.APPDATA = '/some/appdata'; + process.env.LOCALAPPDATA = '/some/localappdata'; + + const result = expandEnvAndResolvePaths(['%APPDATA%/file', '%LOCALAPPDATA%\\other']); + + if (process.platform === 'win32') { + expect(result).toEqual(['/some/appdata/file', '/some/localappdata/other']); + } else { + // on non-windows we expect backslashes to be preserved here + expect(result).toEqual(['/some/appdata/file', '/some/localappdata\\other']); + } + }); + + it('converts backslashes to forward slashes on win32', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const result = expandEnvAndResolvePaths(['C:\\path\\to\\file', 'nochange/needed']); + expect(result).toEqual(['C:/path/to/file', 'nochange/needed']); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); + + it('handles docker bind src path conversion on Windows', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const arg = 'type=bind,src=C:\\path\\to\\dir,dst=/data'; + const result = expandEnvAndResolvePaths([arg]); + // allow a possible current-working-directory prefix (seen on some environments), + // but ensure the drive letter path was converted to /c/path/to/dir or kept as C:/path/to/dir + expect(result[0]).toMatch( + /type=bind,src=(?:.*(?:\/c\/path\/to\/dir|\/[A-Za-z]:\/path\/to\/dir)),dst=\/data/ + ); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); + + it('does not alter docker bind src path on non-Windows', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'linux' }); + try { + const arg = 'type=bind,src=/home/user/dir,dst=/data'; + const result = expandEnvAndResolvePaths([arg]); + expect(result).toEqual([arg]); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); +}); + +describe('MultiServerMCPClient', () => { + beforeEach(() => { + // ensure predictable env for merging tests + process.env.TEST_ORIG_ENV = 'orig'; + jest.resetAllMocks(); + }); + + afterEach(() => { + delete process.env.TEST_ORIG_ENV; + }); + + it('returns empty when no mcp settings', () => { + jest.spyOn(MCP, 'loadMCPSettings').mockReturnValue(null); + + const result = MCP.makeMcpServersFromSettings('/cfg', ['cluster1']); + + // Cannot reliably assert the internal call to loadMCPSettings when both functions + // are in the same module (jest.spyOn does not intercept internal local references), + // so only assert the returned result. + expect(result).toEqual({}); + }); + + it('returns empty when mcp is disabled or has no servers', () => { + jest.spyOn(MCP, 'loadMCPSettings').mockReturnValue({ enabled: false, servers: [] }); + expect(MCP.makeMcpServersFromSettings('/cfg', ['c'])).toEqual({}); + + jest.spyOn(MCP, 'loadMCPSettings').mockReturnValue({ enabled: true, servers: [] }); + expect(MCP.makeMcpServersFromSettings('/cfg', ['c'])).toEqual({}); + }); + + it('filters out disabled or invalid servers and builds server entries', () => { + const mcpSettings = { + enabled: true, + servers: [ + { + name: 'valid', + command: 'cmd', + args: ['arg1'], + enabled: true, + env: { MCP_VAR: 'mcp' }, + }, + { + name: 'disabled', + command: 'cmd', + args: [], + enabled: false, + }, + { + // missing command + name: 'nocmd', + command: '', + args: [], + enabled: true, + }, + { + // missing name + name: '', + command: 'cmd', + args: [], + enabled: true, + }, + ], + }; + + (loadSettings as jest.Mock).mockReturnValue({ mcp: mcpSettings }); + + const result = MCP.makeMcpServersFromSettings('/cfg', ['clusterA']); + + expect(result).toHaveProperty('valid'); + expect(Object.keys(result)).toEqual(['valid']); + + const entry = result['valid'] as any; + expect(entry.transport).toBe('stdio'); + expect(entry.command).toBe('cmd'); + expect(entry.args).toEqual(['arg1']); + // env should include process.env and server.env overrides + expect(entry.env.MCP_VAR).toBe('mcp'); + expect(entry.env.TEST_ORIG_ENV).toBe('orig'); + // restart settings + expect(entry.restart).toBeDefined(); + expect(entry.restart.enabled).toBe(true); + expect(entry.restart.maxAttempts).toBe(3); + expect(entry.restart.delayMs).toBe(2000); + }); + + it('expands HEADLAMP_CURRENT_CLUSTER placeholder using provided clusters[0]', () => { + const mcpSettings = { + enabled: true, + servers: [ + { + name: 'withCluster', + command: 'cmd', + args: ['connect', 'HEADLAMP_CURRENT_CLUSTER'], + enabled: true, + }, + ], + }; + + (loadSettings as jest.Mock).mockReturnValue({ mcp: mcpSettings }); + + const result = MCP.makeMcpServersFromSettings('/cfg', ['my-current-cluster']); + + expect(result).toHaveProperty('withCluster'); + const entry = result['withCluster'] as any; + // the expand function should have replaced the placeholder + expect(entry.args).toEqual(['connect', 'my-current-cluster']); + }); +}); + +describe('settingsChanges', () => { + it('reports enabling and added servers when current is null', () => { + const nextSettings = { + enabled: true, + servers: [{ name: 's1', command: 'cmd1', args: [], enabled: true }], + }; + + const result = MCP.settingsChanges(null, nextSettings as any); + expect(result).toContain('• MCP will be ENABLED'); + expect(result).toContain('• ADD server: "s1" (cmd1)'); + }); + + it('returns empty array when both current and next settings are null', () => { + const result = MCP.settingsChanges(null, null as any); + expect(result).toEqual([]); + }); + + it('reports disabling and removed servers when next is null', () => { + const current = { + enabled: true, + servers: [ + { name: 's1', command: 'cmd1', args: [], enabled: true }, + { name: 's2', command: 'cmd2', args: [], enabled: true }, + ], + }; + + const result = MCP.settingsChanges(current as any, null as any); + expect(result).toContain('• MCP will be DISABLED'); + expect(result).toContain('• REMOVE server: "s1"'); + expect(result).toContain('• REMOVE server: "s2"'); + }); + + it('reports disabling when enabled -> disabled and no servers', () => { + const current = { enabled: true, servers: [] }; + const next = { enabled: false, servers: [] }; + + const result = MCP.settingsChanges(current as any, next as any); + expect(result).toEqual(['• MCP will be DISABLED']); + }); + + it('detects added, removed and modified servers including command/args/env/enable changes', () => { + const current = { + enabled: true, + servers: [ + { name: 'keep', command: 'cmd', args: ['a'], enabled: true, env: { X: '1' } }, + { name: 'removed', command: 'rm', args: [], enabled: true }, + { name: 'modified', command: 'old', args: ['one'], enabled: true, env: { A: 'a' } }, + ], + }; + + const next = { + enabled: true, + servers: [ + { name: 'keep', command: 'cmd', args: ['a'], enabled: true, env: { X: '1' } }, // unchanged + { name: 'added', command: 'new', args: [], enabled: true }, // new + { + name: 'modified', + command: 'newcmd', + args: ['one', 'two'], + enabled: false, // toggled + env: { A: 'b' }, // changed + }, + ], + }; + + const result = MCP.settingsChanges(current as any, next as any); + + expect(result).toEqual( + expect.arrayContaining(['• ADD server: "added" (new)', '• REMOVE server: "removed"']) + ); + + // find the modify message for 'modified' server + const modifyMsg = result.find(r => r.startsWith('• MODIFY server "modified"')); + expect(modifyMsg).toBeDefined(); + // should mention enable/disable, command change, args change, and env change + expect(modifyMsg).toMatch(/enable|disable/); + expect(modifyMsg).toMatch(/change command: "old" → "newcmd"/); + expect(modifyMsg).toMatch(/change arguments: \["one"\] → \["one","two"\]/); + expect(modifyMsg).toMatch(/change environment variables/); + }); + + it('returns empty array when there are no changes', () => { + const s = { + enabled: true, + servers: [{ name: 's', command: 'c', args: ['x'], enabled: true, env: { K: 'v' } }], + }; + + const result = MCP.settingsChanges(s as any, s as any); + expect(result).toEqual([]); + }); +}); diff --git a/app/electron/mcp/MCPSettings.ts b/app/electron/mcp/MCPSettings.ts new file mode 100644 index 00000000000..a6113ed71c5 --- /dev/null +++ b/app/electron/mcp/MCPSettings.ts @@ -0,0 +1,335 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ClientConfig } from '@langchain/mcp-adapters'; +import { type BrowserWindow, dialog } from 'electron'; +import os from 'os'; +import path from 'path'; +import { loadSettings, saveSettings } from '../settings'; + +const DEBUG = true; + +export interface MCPSettings { + /** + * Whether MCP is enabled or not + */ + enabled: boolean; + /** + * List of MCP servers + */ + servers: MCPServer[]; +} + +interface MCPServer { + /** + * Server name + */ + name: string; + /** + * Command to run the MCP tool + */ + command: string; + /** + * Arguments for the MCP tool command + */ + args: string[]; + /** + * Whether the MCP server is enabled or not + */ + enabled: boolean; + /** + * Environment variables for the MCP tool command + */ + env?: Record; +} + +/** + * Load MCP server configuration from settings + * + * @param settingsPath - path to settings file + * @returns MCP settings or null if not found + */ +export function loadMCPSettings(settingsPath: string): MCPSettings | null { + const settings = loadSettings(settingsPath); + if (!settings || typeof settings !== 'object') { + return null; + } + + const mcp = (settings as any).mcp; + return mcp ? (mcp as MCPSettings) : null; +} + +/** + * Save MCP server configuration to settings + * + * @param settingsPath - path to settings file + * @param mcpSettings - MCP settings to save + */ +export function saveMCPSettings(settingsPath: string, mcpSettings: MCPSettings): void { + const settings = loadSettings(settingsPath); + settings.mcp = mcpSettings; + saveSettings(settingsPath, settings); +} + +/** + * Expand environment variables and resolve paths in arguments. + * + * @param args - The array of argument strings to expand. + * @param currentCluster - The current cluster name to replace HEADLAMP_CURRENT_CLUSTER. + * @param cluster - The specific cluster name to replace HEADLAMP_CURRENT_CLUSTER, if provided. + * + * @returns The array of expanded argument strings. + */ +export function expandEnvAndResolvePaths(args: string[], cluster: string | null = null): string[] { + return args.map(arg => { + // Replace Windows environment variables like %USERPROFILE% + let expandedArg = arg; + + // Handle HEADLAMP_CURRENT_CLUSTER placeholder + if (expandedArg.includes('HEADLAMP_CURRENT_CLUSTER')) { + expandedArg = expandedArg.replace(/HEADLAMP_CURRENT_CLUSTER/g, cluster || ''); + } + + // Handle %USERPROFILE% + if (expandedArg.includes('%USERPROFILE%')) { + expandedArg = expandedArg.replace(/%USERPROFILE%/g, os.homedir()); + } + + // Handle other common Windows environment variables + if (expandedArg.includes('%APPDATA%')) { + expandedArg = expandedArg.replace(/%APPDATA%/g, process.env.APPDATA || ''); + } + + if (expandedArg.includes('%LOCALAPPDATA%')) { + expandedArg = expandedArg.replace(/%LOCALAPPDATA%/g, process.env.LOCALAPPDATA || ''); + } + + // Convert Windows backslashes to forward slashes for Docker + if (process.platform === 'win32' && expandedArg.includes('\\')) { + expandedArg = expandedArg.replace(/\\/g, '/'); + } + + // Handle Docker volume mount format and ensure proper Windows path format + if (expandedArg.includes('type=bind,src=')) { + const match = expandedArg.match(/type=bind,src=(.+?),dst=(.+)/); + if (match) { + let srcPath = match[1]; + const dstPath = match[2]; + + // Resolve the source path + if (process.platform === 'win32') { + srcPath = path.resolve(srcPath); + // For Docker on Windows, we might need to convert C:\ to /c/ format + if (srcPath.match(/^[A-Za-z]:/)) { + srcPath = '/' + srcPath.charAt(0).toLowerCase() + srcPath.slice(2).replace(/\\/g, '/'); + } + } + + expandedArg = `type=bind,src=${srcPath},dst=${dstPath}`; + } + } + + return expandedArg; + }); +} + +/** + * Make mpcServers from settings for the mpcServers arg of MultiServerMCPClient. + * + * @param settingsPath - path to settings file + * @param clusters - list of current clusters + * + * @returns Record of MCP servers + */ +export function makeMcpServersFromSettings( + settingsPath: string, + clusters: string[] +): ClientConfig['mcpServers'] { + const mcpServers: ClientConfig['mcpServers'] = {}; + + const mcpSettings = loadMCPSettings(settingsPath); + if ( + !mcpSettings || + !mcpSettings.enabled || + !mcpSettings.servers || + mcpSettings.servers.length === 0 + ) { + return mcpServers; + } + + for (const server of mcpSettings.servers) { + if (!server.enabled || !server.name || !server.command) { + continue; + } + + const expandedArgs = expandEnvAndResolvePaths(server.args || [], clusters[0] || null); + + if (DEBUG) { + console.log(`Expanded args for ${server.name}:`, expandedArgs); + } + + const serverEnv = server.env ? { ...process.env, ...server.env } : process.env; + + mcpServers[server.name] = { + transport: 'stdio', + command: server.command, + args: expandedArgs, + env: serverEnv as Record, + restart: { + enabled: true, + maxAttempts: 3, + delayMs: 2000, + }, + }; + } + + return mcpServers; +} + +/** + * settingsChanges returns a list of human-readable descriptions of changes + * between the current MCP settings and next MCP settings. + * + * @param currentSettings - The current MCP settings, or null if none exist. + * @param nextSettings - The next MCP settings to compare against. + * + * @returns An array of strings describing the changes. + */ +export function settingsChanges( + currentSettings: MCPSettings | null, + nextSettings: MCPSettings | null +): string[] { + const changes: string[] = []; + + // Check if MCP is being enabled/disabled + const currentEnabled = currentSettings?.enabled ?? false; + const nextEnabled = nextSettings?.enabled ?? false; + + if (currentEnabled !== nextEnabled) { + changes.push(`• MCP will be ${nextEnabled ? 'ENABLED' : 'DISABLED'}`); + } + + // Get current and next server lists + const currentServers = currentSettings?.servers ?? []; + const nextServers = nextSettings?.servers ?? []; + + // Check for added servers + const currentServerNames = new Set(currentServers.map(s => s.name)); + const nextServerNames = new Set(nextServers.map(s => s.name)); + + for (const server of nextServers) { + if (!currentServerNames.has(server.name)) { + changes.push(`• ADD server: "${server.name}" (${server.command})`); + } + } + + // Check for removed servers + for (const server of currentServers) { + if (!nextServerNames.has(server.name)) { + changes.push(`• REMOVE server: "${server.name}"`); + } + } + + // Check for modified servers + for (const nextServer of nextServers) { + const currentServer = currentServers.find(s => s.name === nextServer.name); + if (currentServer) { + const serverChanges: string[] = []; + + // Check enabled status + if (currentServer.enabled !== nextServer.enabled) { + serverChanges.push(`${nextServer.enabled ? 'enable' : 'disable'}`); + } + + // Check command + if (currentServer.command !== nextServer.command) { + serverChanges.push(`change command: "${currentServer.command}" → "${nextServer.command}"`); + } + + // Check arguments + const currentArgs = JSON.stringify(currentServer.args || []); + const nextArgs = JSON.stringify(nextServer.args || []); + if (currentArgs !== nextArgs) { + serverChanges.push(`change arguments: ${currentArgs} → ${nextArgs}`); + } + + // Check environment variables + const currentEnv = JSON.stringify(currentServer.env || {}); + const nextEnv = JSON.stringify(nextServer.env || {}); + if (currentEnv !== nextEnv) { + serverChanges.push(`change environment variables`); + } + + if (serverChanges.length > 0) { + changes.push(`• MODIFY server "${nextServer.name}": ${serverChanges.join(', ')}`); + } + } + } + + return changes; +} + +/** + * Shows a dialog asking user for confirmation if MCP settings changes are ok. + * + * Displays a summary of changes between currentSettings and nextSettings. + * + * @param mainWindow - The main BrowserWindow to parent the dialog. + * @param currentSettings - Current MCP settings, or null if none exists. + * @param nextSettings - New MCP settings to be applied. + * + * @returns Promise resolving to true if user approves changes, false if cancelled. + */ +export async function showSettingsChangeDialog( + mainWindow: BrowserWindow, + currentSettings: MCPSettings | null, + nextSettings: MCPSettings +): Promise { + const changes = settingsChanges(currentSettings, nextSettings); + const result = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Apply Changes', 'Cancel'], + defaultId: 1, + title: 'MCP Settings Changes', + message: 'The application wants to update the MCP settings.', + detail: + changes.length > 0 + ? `The following changes will be applied:\n\n${changes.join( + '\n' + )}\n\nDo you want to apply these changes?` + : 'No changes detected in the MCP settings.\n\nDo you want to proceed anyway?', + }); + return result.response === 0; // 0 is "Apply Changes" +} + +/** + * Check if any server in the settings uses HEADLAMP_CURRENT_CLUSTER placeholder. + * This determines whether the MCP client needs to be restarted on cluster changes. + * + * @param settingsPath - path to settings file + * + * @returns True if any enabled server has HEADLAMP_CURRENT_CLUSTER in its arguments + */ +export function hasClusterDependentServers(settingsPath: string): boolean { + return ( + loadMCPSettings(settingsPath)?.servers.some( + server => + server.enabled && + server.args && + server.args.some(arg => arg.includes('HEADLAMP_CURRENT_CLUSTER')) + ) || false + ); +} diff --git a/app/electron/mcp/MCPToolStateStore.test.ts b/app/electron/mcp/MCPToolStateStore.test.ts new file mode 100644 index 00000000000..65a59043dc4 --- /dev/null +++ b/app/electron/mcp/MCPToolStateStore.test.ts @@ -0,0 +1,506 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { + MCPToolStateStore, + parseServerNameToolName, + summarizeMcpToolStateChanges, + validateToolArgs, +} from './MCPToolStateStore'; + +function tmpPath(): string { + return path.join(os.tmpdir(), `mcp-test-${Date.now()}-${Math.random()}.json`); +} + +describe('MCPConfig', () => { + let toolStatePath: string; + + beforeEach(() => { + toolStatePath = tmpPath(); + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + it('defaults to enabled for unknown server/tool', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + expect(toolState.isToolEnabled('cluster-x', 'tool-a')).toBe(true); + }); + + it('setToolEnabled updates state and getDisabled/getEnabled reflect it', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + toolState.setToolEnabled('cluster-a', 'tool-1', false); + expect(toolState.isToolEnabled('cluster-a', 'tool-1')).toBe(false); + expect(toolState.getDisabledTools('cluster-a')).toContain('tool-1'); + expect(toolState.getEnabledTools('cluster-a')).not.toContain('tool-1'); + + toolState.setToolEnabled('cluster-a', 'tool-1', true); + expect(toolState.isToolEnabled('cluster-a', 'tool-1')).toBe(true); + expect(toolState.getEnabledTools('cluster-a')).toContain('tool-1'); + }); + + it('recordToolUsage increments usageCount and sets lastUsed (in-memory)', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + toolState.recordToolUsage('cluster-u', 'tool-u'); + toolState.recordToolUsage('cluster-u', 'tool-u'); + + const stats = toolState.getToolStats('cluster-u', 'tool-u'); + expect(stats).not.toBeNull(); + expect(stats?.usageCount).toBe(2); + expect(stats?.lastUsed).toBeInstanceOf(Date); + }); + + it('persists enabled state and usage to disk across instances', async () => { + const cfg1 = new MCPToolStateStore(toolStatePath); + await cfg1.initialize(); + cfg1.setToolEnabled('cluster-p', 'tool-p', false); + cfg1.recordToolUsage('cluster-p', 'tool-p'); + + // create a fresh instance which loads from the same file + const cfg2 = new MCPToolStateStore(toolStatePath); + await cfg2.initialize(); + expect(cfg2.isToolEnabled('cluster-p', 'tool-p')).toBe(false); + + const stats = cfg2.getToolStats('cluster-p', 'tool-p'); + // After load from JSON, lastUsed becomes a string; usageCount should persist as number + expect(stats?.usageCount).toBe(1); + expect( + typeof (stats as any)?.lastUsed === 'string' || (stats as any)?.lastUsed instanceof Date + ).toBe(true); + }); + + it('initializeToolsConfig creates and updates schemas and descriptions', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + const schemaA = { type: 'object', properties: { a: { type: 'string' } } }; + toolState.initializeToolsConfig('srv', [ + { name: 'tool-x', inputSchema: schemaA, description: 'desc x' }, + { name: 'tool-y', inputSchema: { type: 'string' }, description: 'desc y' }, + ]); + + const s1 = toolState.getToolStats('srv', 'tool-x'); + expect(s1).not.toBeNull(); + expect((s1 as any).inputSchema).toEqual(schemaA); + expect((s1 as any).description).toBe('desc x'); + + // re-initialize with changed schema/description for tool-x + const schemaA2 = { type: 'object', properties: { a: { type: 'number' } } }; + toolState.initializeToolsConfig('srv', [ + { name: 'tool-x', inputSchema: schemaA2, description: 'desc x updated' }, + ]); + + const s2 = toolState.getToolStats('srv', 'tool-x'); + expect((s2 as any).inputSchema).toEqual(schemaA2); + expect((s2 as any).description).toBe('desc x updated'); + }); + + it('replaceToolsConfig preserves enabled state and usageCount and removes missing tools', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + // create initial tools and modify state + toolState.initializeToolsConfig('cluster-r', [ + { name: 'keep-tool', inputSchema: null, description: 'keep' }, + { name: 'drop-tool', inputSchema: null, description: 'drop' }, + ]); + toolState.setToolEnabled('cluster-r', 'keep-tool', false); + // increment usage for keep-tool + toolState.recordToolUsage('cluster-r', 'keep-tool'); + toolState.recordToolUsage('cluster-r', 'keep-tool'); + + // replace with only keep-tool (and maybe new-tool) + toolState.replaceToolsConfig({ + 'cluster-r': [ + { name: 'keep-tool', inputSchema: null, description: 'keep' }, + { name: 'new-tool', inputSchema: null, description: 'new' }, + ], + }); + + // dropped tool should be gone + expect(toolState.getToolStats('cluster-r', 'drop-tool')).toBeNull(); + + // keep-tool should preserve enabled and usageCount + const keep = toolState.getToolStats('cluster-r', 'keep-tool'); + expect(keep).not.toBeNull(); + expect(keep?.usageCount).toBe(2); + expect(keep?.enabled).toBe(false); + + // new-tool should exist with defaults + const n = toolState.getToolStats('cluster-r', 'new-tool'); + expect(n).not.toBeNull(); + expect(n?.usageCount).toBe(0); + expect(n?.enabled).toBe(true); + }); + + it('getConfig, setConfig, replaceConfig and resetConfig behave as expected', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + toolState.resetConfig(); + expect(Object.keys(toolState.getConfig())).toHaveLength(0); + + const newConf = { + s1: { + t1: { enabled: false, usageCount: 3, inputSchema: null, description: 'd' }, + }, + }; + toolState.setConfig(newConf); + expect(toolState.getConfig()).toEqual(newConf); + + // replaceConfig should overwrite entirely + const replaced = { + s2: { + t2: { enabled: true, usageCount: 0, inputSchema: null, description: '' }, + }, + }; + toolState.replaceConfig(replaced); + expect(toolState.getConfig()).toEqual(replaced); + + // persisted to disk and load into new instance + const toolState2 = new MCPToolStateStore(toolStatePath); + await toolState2.initialize(); + expect(toolState2.getConfig()).toEqual(replaced); + }); +}); + +describe('parseServerNameToolName', () => { + it('returns default serverName when no separator is present', () => { + const res = parseServerNameToolName('kubectl'); + expect(res.serverName).toBe('default'); + expect(res.toolName).toBe('kubectl'); + }); + + it('splits server and tool when a single separator is present', () => { + const res = parseServerNameToolName('myserver__helm'); + + expect(res.serverName).toBe('myserver'); + expect(res.toolName).toBe('helm'); + }); + + it('preserves additional separators in the toolName when multiple separators are present', () => { + const res = parseServerNameToolName('myserver__helm__test'); + expect(res.serverName).toBe('myserver'); + expect(res.toolName).toBe('helm__test'); + }); +}); + +describe('summarizeMcpToolStateChanges', () => { + it('returns zero changes for identical empty configs', () => { + const res = summarizeMcpToolStateChanges({}, {}); + expect(res.totalChanges).toBe(0); + expect(res.summaryText).toBe(''); + }); + + it('counts added enabled tools and includes them in ENABLE summary', () => { + const current = {}; + const nw = { + srv1: { + 'tool-a': { enabled: true }, + }, + }; + const res = summarizeMcpToolStateChanges(current, nw); + // addedTools (1) + enabledTools (1) => total 2 + expect(res.totalChanges).toBe(2); + expect(res.summaryText).toContain('✓ ENABLE (1)'); + expect(res.summaryText).toContain('tool-a (srv1)'); + expect(res.summaryText).not.toContain('✗ DISABLE'); + }); + + it('counts removed tools even when no summary lines are produced', () => { + const current = { + srv1: { + 'tool-x': { enabled: true }, + }, + }; + const nw = {}; + const res = summarizeMcpToolStateChanges(current, nw); + // removedTools (1) => total 1 + expect(res.totalChanges).toBe(1); + // removed tools are not printed in summaryText, so should be empty + expect(res.summaryText).toBe(''); + }); + + it('detects enable/disable changes between configs', () => { + const current = { + srvA: { + 'tool-1': { enabled: true }, + 'tool-2': { enabled: false }, + }, + }; + const nw = { + srvA: { + 'tool-1': { enabled: false }, // changed to disabled + 'tool-2': { enabled: true }, // changed to enabled + }, + }; + const res = summarizeMcpToolStateChanges(current, nw); + // two changes (one disabled, one enabled) + expect(res.totalChanges).toBe(2); + expect(res.summaryText).toContain('✓ ENABLE (1): tool-2 (srvA)'); + expect(res.summaryText).toContain('✗ DISABLE (1): tool-1 (srvA)'); + // Ensure ENABLE and DISABLE sections are separated by a blank line + expect(res.summaryText.split('\n\n').length).toBeGreaterThanOrEqual(2); + }); + + it('aggregates changes across multiple servers', () => { + // This test verifies that summarizeMcpToolConfigChanges correctly + // aggregates changes across multiple servers, counting: + // - added tools (whether enabled or disabled), + // - removed tools (which are counted but not included in the human-readable summary), + // - tools that changed enabled state (enable -> disable and vice-versa). + // It ensures both the numeric totals and the generated ENABLE/DISABLE summary + // lines include the expected entries and server names. + const current = { + s1: { a: { enabled: true } }, + s2: { x: { enabled: false } }, + }; + const nw = { + s1: { a: { enabled: false }, b: { enabled: true } }, // a->disabled, b added+enabled + s2: { + /* x removed */ + }, + s3: { y: { enabled: false } }, // new disabled + }; + const res = summarizeMcpToolStateChanges(current, nw); + // Changes: a (changed), b (added), x (removed), y (added) + // enabledTools: b (added enabled) => 1 + // disabledTools: a (changed), y (added disabled) => 2 + // addedTools: b,y => 2 + // removedTools: x => 1 + // total = 1+2+2+1 = 6 + expect(res.totalChanges).toBe(6); + expect(res.summaryText).toContain('✓ ENABLE (1)'); + expect(res.summaryText).toContain('✗ DISABLE (2)'); + expect(res.summaryText).toContain('b (s1)'); + expect(res.summaryText).toContain('a (s1)'); + expect(res.summaryText).toContain('y (s3)'); + }); + + it('ignores non-enabled metadata-only changes (description/inputSchema)', () => { + const current = { + srvM: { + 'tool-meta': { enabled: true, description: 'old', inputSchema: { type: 'string' } }, + }, + }; + const nw = { + srvM: { + 'tool-meta': { enabled: true, description: 'new', inputSchema: { type: 'string' } }, + }, + }; + const res = summarizeMcpToolStateChanges(current, nw); + // Only metadata changed; enabled state unchanged => no counted changes + expect(res.totalChanges).toBe(0); + expect(res.summaryText).toBe(''); + }); +}); + +describe('initConfigFromClientTools', () => { + let toolStatePath: string; + + beforeEach(() => { + toolStatePath = tmpPath(); + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(toolStatePath)) fs.unlinkSync(toolStatePath); + } catch { + // ignore + } + }); + + it('clears config when no client tools are provided', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + // Seed with some config + toolState.setConfig({ + someServer: { + someTool: { enabled: false, usageCount: 2, inputSchema: null, description: '' }, + }, + }); + expect(Object.keys(toolState.getConfig()).length).toBeGreaterThan(0); + + // init with empty client tools should clear the config + toolState.initConfigFromClientTools([]); + expect(Object.keys(toolState.getConfig())).toHaveLength(0); + }); + + it('groups tools by server, extracts schema and description, and sets defaults', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + const clientTools: any[] = [ + { + name: 'srvA__tool-x', + schema: { type: 'object', properties: { a: { type: 'string' } } }, + description: 'desc x', + }, + { name: 'tool-no-server', description: 'global tool' }, // no schema provided + ]; + + toolState.initConfigFromClientTools(clientTools); + + const sx = toolState.getToolStats('srvA', 'tool-x'); + expect(sx).not.toBeNull(); + expect((sx as any).inputSchema).toEqual({ + type: 'object', + properties: { a: { type: 'string' } }, + }); + expect((sx as any).description).toBe('desc x'); + expect(sx?.enabled).toBe(true); + expect(sx?.usageCount).toBe(0); + + const gn = toolState.getToolStats('default', 'tool-no-server'); + expect(gn).not.toBeNull(); + expect((gn as any).inputSchema).toBeNull(); + expect((gn as any).description).toBe('global tool'); + expect(gn?.enabled).toBe(true); + }); + + it('preserves enabled state and usageCount from existing config when tool still exists', async () => { + const toolState = new MCPToolStateStore(toolStatePath); + await toolState.initialize(); + + // Seed with a tool that has specific enabled/usage values + toolState.setConfig({ + myServer: { + preservedTool: { + enabled: false, + usageCount: 5, + inputSchema: null, + description: 'old desc', + }, + }, + }); + + // Client reports same tool (with updated schema/description) + const clientTools: any[] = [ + { name: 'myServer__preservedTool', schema: { type: 'string' }, description: 'new desc' }, + ]; + + toolState.initConfigFromClientTools(clientTools); + + const p = toolState.getToolStats('myServer', 'preservedTool'); + expect(p).not.toBeNull(); + // preserved values should remain + expect(p?.enabled).toBe(false); + expect(p?.usageCount).toBe(5); + // schema/description should be updated from client tools + expect((p as any).inputSchema).toEqual({ type: 'string' }); + expect((p as any).description).toBe('new desc'); + }); +}); + +describe('validateToolArgs', () => { + it('returns valid when schema is null', () => { + const res = validateToolArgs(null, { any: 1 }); + expect(res.valid).toBe(true); + expect(res.error).toBeUndefined(); + }); + + it('fails when a required property is missing', () => { + const schema = { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + }, + }; + const res = validateToolArgs(schema, {}); + expect(res.valid).toBe(false); + expect(res.error).toContain("Required parameter 'name'"); + }); + + it('fails when property type does not match (number expected)', () => { + const schema = { + type: 'object', + properties: { + age: { type: 'number' }, + }, + }; + const res = validateToolArgs(schema, { age: 'not-a-number' }); + expect(res.valid).toBe(false); + expect(res.error).toContain('should be a number'); + }); + + it('validates array types correctly', () => { + const schema = { + type: 'object', + properties: { + items: { type: 'array' }, + }, + }; + const ok = validateToolArgs(schema, { items: [1, 2, 3] }); + expect(ok.valid).toBe(true); + + const notOk = validateToolArgs(schema, { items: 'not-an-array' }); + expect(notOk.valid).toBe(false); + expect(notOk.error).toContain('should be an array'); + }); + + it('validates object types and rejects arrays/null for object type', () => { + const schema = { + type: 'object', + properties: { + cfg: { type: 'object' }, + }, + }; + expect(validateToolArgs(schema, { cfg: { a: 1 } }).valid).toBe(true); + const asArray = validateToolArgs(schema, { cfg: [1, 2] }); + expect(asArray.valid).toBe(false); + expect(asArray.error).toContain('should be an object'); + const asNull = validateToolArgs(schema, { cfg: null }); + expect(asNull.valid).toBe(false); + expect(asNull.error).toContain('should be an object'); + }); + + it('treats unsupported property types as non-fatal and returns valid', () => { + const schema = { + type: 'object', + properties: { + count: { type: 'integer' }, // unsupported type in validator + }, + }; + const res = validateToolArgs(schema, { count: 42 }); + expect(res.valid).toBe(true); + expect(res.error).toBeUndefined(); + }); +}); diff --git a/app/electron/mcp/MCPToolStateStore.ts b/app/electron/mcp/MCPToolStateStore.ts new file mode 100644 index 00000000000..148b5d40ed8 --- /dev/null +++ b/app/electron/mcp/MCPToolStateStore.ts @@ -0,0 +1,707 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DynamicStructuredTool } from '@langchain/core/dist/tools/index'; +import { type BrowserWindow, dialog } from 'electron'; +import * as fs from 'fs'; + +/** + * State of a single MCP tool. + */ +export interface MCPToolState { + /** + * Whether the tool is enabled or disabled + */ + enabled: boolean; + /** + * Timestamp of the last time the tool was used + */ + lastUsed?: Date; + /** + * Number of times the tool has been used + */ + usageCount?: number; + /** + * JSON schema for tool parameters + */ + inputSchema?: any; + /** + * Description of the tool from MCP server + */ + description?: string; +} + +/** + * State of all MCP tools for a specific server. + */ +export interface MCPServerToolState { + [toolName: string]: MCPToolState; +} + +/** + * Configuration for MCP tools across multiple servers. + */ +export interface MCPToolsConfig { + [serverName: string]: MCPServerToolState; +} + +/** + * Create a summary of changes between two MCP tool configurations. + * + * @param currentConfig - The current MCP tools configuration. + * @param newConfig - The new MCP tools configuration. + * + * @returns An object containing the total number of changes and a summary text. + */ +export function summarizeMcpToolStateChanges( + currentConfig: Record>, + newConfig: Record> +): { totalChanges: number; summaryText: string } { + const enabledTools: string[] = []; + const disabledTools: string[] = []; + const addedTools: string[] = []; + const removedTools: string[] = []; + + // Get all server names from both configs + const allServers = new Set([ + ...Object.keys(currentConfig || {}), + ...Object.keys(newConfig || {}), + ]); + + for (const serverName of allServers) { + const currentServerConfig = currentConfig[serverName] || {}; + const newServerConfig = newConfig[serverName] || {}; + + // Get all tool names from both configs + const allTools = new Set([ + ...Object.keys(currentServerConfig), + ...Object.keys(newServerConfig), + ]); + + for (const toolName of allTools) { + const currentTool = currentServerConfig[toolName]; + const newTool = newServerConfig[toolName]; + const displayName = `${toolName} (${serverName})`; + + if (!currentTool && newTool) { + // New tool added + addedTools.push(displayName); + if (newTool.enabled) { + enabledTools.push(displayName); + } else { + disabledTools.push(displayName); + } + } else if (currentTool && !newTool) { + // Tool removed + removedTools.push(displayName); + } else if (currentTool && newTool) { + // Tool modified + if (currentTool.enabled !== newTool.enabled) { + if (newTool.enabled) { + enabledTools.push(displayName); + } else { + disabledTools.push(displayName); + } + } + } + } + } + + // Build summary text + const summaryParts: string[] = []; + + if (enabledTools.length > 0) { + summaryParts.push(`✓ ENABLE (${enabledTools.length}): ${enabledTools.join(', ')}`); + } + + if (disabledTools.length > 0) { + summaryParts.push(`✗ DISABLE (${disabledTools.length}): ${disabledTools.join(', ')}`); + } + + const totalChanges = + enabledTools.length + disabledTools.length + addedTools.length + removedTools.length; + + return { + totalChanges, + summaryText: summaryParts.join('\n\n'), + }; +} + +/** + * MCPToolStateStore manages configuration for MCP (Multi-Cluster Platform) + * tools, including enabled/disabled state and usage statistics. + * + * Example: + * ```ts + * const toolStatePath = path.join(app.getPath('userData'), 'mcp-tools-config.json'); + * const toolState = new MCPToolStateStore(toolStatePath); + * + * // Example inputSchema + * const exampleSchema = { + * type: 'object', + * properties: { + * param1: { type: 'string', description: 'Parameter 1' }, + * param2: { type: 'number', description: 'Parameter 2' }, + * }, + * required: ['param1'], + * }; + * + * // Initialize default config for available tools + * toolState.initializeToolsConfig('my-cluster', [ + * { name: 'tool-a', inputSchema: exampleSchema, description: 'Tool A description' }, + * { name: 'tool-b', inputSchema: exampleSchema, description: 'Tool B description' }, + * ]); + * + * // Enable or disable a tool + * toolState.setToolEnabled('my-cluster', 'tool-a', false); + * + * // Check if a tool is enabled + * const isEnabled = toolState.isToolEnabled('my-cluster', 'tool-a'); + * + * // Get all disabled tools for a server + * const disabledTools = toolState.getDisabledTools('my-cluster'); + * + * // Record tool usage + * toolState.recordToolUsage('my-cluster', 'tool-a'); + * + * // Get tool statistics + * const toolStats = toolState.getToolStats('my-cluster', 'tool-a'); + * + * // Replace entire tools configuration + * toolState.replaceToolsConfig({ + * 'my-cluster': [ + * { name: 'tool-a', inputSchema: {...}, description: 'Tool A description' }, + * { name: 'tool-b', inputSchema: {...}, description: 'Tool B description' }, + * ], + * 'another-cluster': [ + * { name: 'tool-c', inputSchema: {...}, description: 'Tool C description' }, + * ], + * }); + * + * // Reset configuration + * toolState.resetConfig(); + * + * // Get the complete configuration + * const completeConfig = toolState.getConfig(); + * + * // Set the complete configuration + * toolState.setConfig(completeConfig); + * ``` + */ +export class MCPToolStateStore { + private toolStatePath: string; + private config: MCPToolsConfig = {}; + + constructor(configPath: string) { + this.toolStatePath = configPath; + } + + /** + * Initialize the MCP client. + */ + async initialize(): Promise { + return await this.loadConfig(); + } + + /** + * Load MCP tools configuration from file + */ + private async loadConfig(): Promise { + try { + await fs.promises.access(this.toolStatePath, fs.constants.F_OK); + const configData = await fs.promises.readFile(this.toolStatePath, 'utf-8'); + this.config = JSON.parse(configData); + } catch (error) { + // If file doesn't exist or any error occurs, fall back to empty config + if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') { + this.config = {}; + } else { + console.error('Error loading MCP tools configuration:', error); + this.config = {}; + } + } + } + + /** + * Save MCP tools configuration to file + */ + private saveConfig(): void { + try { + fs.writeFileSync(this.toolStatePath, JSON.stringify(this.config, null, 2), 'utf-8'); + } catch (error) { + console.error('Error saving MCP tools configuration:', error); + } + } + + /** + * Get the enabled state of a specific tool + */ + isToolEnabled(serverName: string, toolName: string): boolean { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + // Default to enabled for new tools + return true; + } + + const toolState = serverConfig[toolName]; + if (!toolState) { + // Default to enabled for new tools + return true; + } + + return toolState.enabled; + } + + /** + * Set the enabled state of a specific tool + */ + setToolEnabled(serverName: string, toolName: string, enabled: boolean): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + if (!this.config[serverName][toolName]) { + this.config[serverName][toolName] = { + enabled: true, + usageCount: 0, + }; + } + + this.config[serverName][toolName].enabled = enabled; + this.saveConfig(); + } + + /** + * Get all disabled tools for a server + */ + getDisabledTools(serverName: string): string[] { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + return []; + } + + return Object.entries(serverConfig) + .filter(([, toolState]) => !toolState.enabled) + .map(([toolName]) => toolName); + } + + /** + * Get all enabled tools for a server + */ + getEnabledTools(serverName: string): string[] { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + return []; + } + + return Object.entries(serverConfig) + .filter(([, toolState]) => toolState.enabled) + .map(([toolName]) => toolName); + } + + /** + * Update tool usage statistics + */ + recordToolUsage(serverName: string, toolName: string): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + if (!this.config[serverName][toolName]) { + this.config[serverName][toolName] = { + enabled: true, + usageCount: 0, + }; + } + + const toolState = this.config[serverName][toolName]; + toolState.lastUsed = new Date(); + toolState.usageCount = (toolState.usageCount || 0) + 1; + this.saveConfig(); + } + + /** + * Get the complete configuration + */ + getConfig(): MCPToolsConfig { + return { ...this.config }; + } + + /** + * Set the complete configuration + */ + setConfig(newConfig: MCPToolsConfig): void { + this.config = { ...newConfig }; + this.saveConfig(); + } + + /** + * Reset configuration to empty state + */ + resetConfig(): void { + this.config = {}; + this.saveConfig(); + } + + /** + * Initialize default configuration for available tools with schemas + */ + initializeToolsConfig( + serverName: string, + toolsInfo: Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + ): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + const serverConfig = this.config[serverName]; + let hasChanges = false; + + for (const toolInfo of toolsInfo) { + const toolName = toolInfo.name; + + if (!serverConfig[toolName]) { + serverConfig[toolName] = { + enabled: true, + usageCount: 0, + inputSchema: toolInfo.inputSchema || null, + description: toolInfo.description || '', + }; + hasChanges = true; + } else { + // Always update schema and description for existing tools + let toolChanged = false; + + // Update schema if it's different or missing + const currentSchema = JSON.stringify(serverConfig[toolName].inputSchema || null); + const newSchema = JSON.stringify(toolInfo.inputSchema || null); + if (currentSchema !== newSchema) { + serverConfig[toolName].inputSchema = toolInfo.inputSchema || null; + toolChanged = true; + } + + // Update description if it's different or missing + const currentDescription = serverConfig[toolName].description || ''; + const newDescription = toolInfo.description || ''; + if (currentDescription !== newDescription) { + serverConfig[toolName].description = newDescription; + toolChanged = true; + } + + if (toolChanged) { + hasChanges = true; + } + } + } + + if (hasChanges) { + this.saveConfig(); + } + } + + /** + * Get tool statistics + */ + getToolStats(serverName: string, toolName: string): MCPToolState | null { + const serverConfig = this.config[serverName]; + if (!serverConfig || !serverConfig[toolName]) { + return null; + } + + return { ...serverConfig[toolName] }; + } + + /** + * Replace the entire tools configuration with a new set of tools + * This overwrites all existing tools with only the current ones + */ + replaceToolsConfig( + toolsByServer: Record< + string, + Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + > + ): void { + // Create a new config object + const newConfig: MCPToolsConfig = {}; + + for (const [serverName, toolsInfo] of Object.entries(toolsByServer)) { + newConfig[serverName] = {}; + + for (const toolInfo of toolsInfo) { + const toolName = toolInfo.name; + + // Check if this tool existed in the old config to preserve enabled state and usage count + const oldToolState = this.config[serverName]?.[toolName]; + + newConfig[serverName][toolName] = { + enabled: oldToolState?.enabled ?? true, // Preserve enabled state or default to true + usageCount: oldToolState?.usageCount ?? 0, // Preserve usage count or default to 0 + inputSchema: toolInfo.inputSchema || null, + description: toolInfo.description || '', + }; + } + } + + // Replace the entire config + this.config = newConfig; + this.saveConfig(); + } + + /** + * Replace the entire configuration with a new config object + */ + replaceConfig(newConfig: MCPToolsConfig): void { + this.config = newConfig; + this.saveConfig(); + } + + /** + * Initialize tools configuration for all available tools from client tools. + * This completely replaces the existing config with current tools. + * + * @param clientTools - Array of available tools with their schemas. From MultiServerMCPClient.getTools() + */ + initConfigFromClientTools(clientTools: DynamicStructuredTool[]): void { + if (!clientTools || clientTools.length === 0) { + console.log('No tools available for configuration initialization'); + // Clear the config if no tools are available + this.replaceConfig({}); + return; + } + + // Group tools by server name with their schemas + const toolsByServer: Record< + string, + Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + > = {}; + + for (const tool of clientTools) { + // Extract server name from tool name (format: "serverName__toolName") + const { serverName, toolName } = parseServerNameToolName(tool.name); + + // Extract schema from the tool (LangChain tools use .schema property) + const toolSchema = tool.schema || (tool as any).inputSchema || null; + console.log( + `Processing tool: ${toolName}, has inputSchema: ${!!toolSchema}, description: "${ + tool.description + }"` + ); + + if (!toolsByServer[serverName]) { + toolsByServer[serverName] = []; + } + + toolsByServer[serverName].push({ + name: toolName, + inputSchema: toolSchema, + description: tool.description || '', + }); + } + + console.log('Tools grouped by server:', Object.keys(toolsByServer)); + + // Replace the entire configuration with current tools + this.replaceToolsConfig(toolsByServer); + } +} + +/** + * Parse tool name to extract server name and tool name components. + * + * @param fullToolName - The full tool name string, potentially including server name. + * + * @returns An object containing serverName and toolName. + * + * @example + * ```ts + * parseServerNameToolName('myserver__helm') + * // returns { serverName: 'myserver', toolName: 'helm' } + * parseServerNameToolName('kubectl') + * // returns { serverName: 'default', toolName: 'kubectl' } + * ``` + */ +export function parseServerNameToolName(fullToolName: string): { + serverName: string; + toolName: string; +} { + const parts = fullToolName.split('__'); + if (parts.length >= 2) { + return { + serverName: parts[0], + toolName: parts.slice(1).join('__'), + }; + } + return { + serverName: 'default', + toolName: fullToolName, + }; +} + +/** + * Validate tool arguments against tool schema. + * + * Note: this validates as true if it doesn't recognize the schema format, + * and validates as true if it doesn't cover the type of the input. + * + * @todo: @langchain/mcp-adapters does validation internally, so this may be redundant? + * + * @param schema - The tool's input schema. + * From toolState.inputSchema from @langchain/mcp-adapters tool input schemas. + * @param args - The arguments to validate. + * + * @returns An object indicating whether the arguments are valid and any error message. + * + * @example + * ```ts + * const schema = { + * type: 'object', + * properties: { + * param1: { type: 'string' }, + * param2: { type: 'number' }, + * }, + * required: ['param1'], + * }; + * const args = { param1: 'value1', param2: 42 }; + * const result = validateToolArgs(schema, args); + * // result: { valid: true } + * ``` + */ +export function validateToolArgs( + schema: MCPToolState['inputSchema'] | null, + args: Record +): { valid: boolean; error?: string } { + if (!schema) { + // No schema available, assume valid + return { valid: true }; + } + + try { + // Basic validation - check required properties + if (schema.required && Array.isArray(schema.required)) { + for (const requiredProp of schema.required) { + if (args[requiredProp] === undefined || args[requiredProp] === null) { + return { + valid: false, + error: `Required parameter '${requiredProp}' is missing`, + }; + } + } + } + + // Check property types if schema properties are defined + if (schema.properties) { + for (const [propName, propSchema] of Object.entries(schema.properties as any)) { + if (args[propName] !== undefined) { + const propType = (propSchema as any).type; + const actualType = typeof args[propName]; + + if (propType === 'string' && actualType !== 'string') { + return { + valid: false, + error: `Parameter '${propName}' should be a string, got ${actualType}`, + }; + } + if (propType === 'number' && actualType !== 'number') { + return { + valid: false, + error: `Parameter '${propName}' should be a number, got ${actualType}`, + }; + } + if (propType === 'boolean' && actualType !== 'boolean') { + return { + valid: false, + error: `Parameter '${propName}' should be a boolean, got ${actualType}`, + }; + } + if (propType === 'array' && !Array.isArray(args[propName])) { + return { + valid: false, + error: `Parameter '${propName}' should be an array, got ${actualType}`, + }; + } + if ( + propType === 'object' && + (actualType !== 'object' || Array.isArray(args[propName]) || args[propName] === null) + ) { + return { + valid: false, + error: `Parameter '${propName}' should be an object, got ${actualType}`, + }; + } + + // If the types are not covered above? We warn, and skip validation. + if (!['string', 'number', 'boolean', 'array', 'object'].includes(propType)) { + console.warn(`Unsupported parameter type in schema: ${propType}`); + + // return { + // valid: false, + // error: `Unsupported parameter type '${propType}' for '${propName}'`, + // }; + } + } + } + } + + return { valid: true }; + } catch (error) { + return { + valid: false, + error: `Schema validation error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }; + } +} + +/** + * Show detailed confirmation dialog for tools configuration changes. + * Compares current and new configurations and displays a summary of changes. + * + * @param mainWindow - The main BrowserWindow to parent the dialog. + * @param currentConfig - The current configuration. + * @param nextConfig - The new configuration to be applied. + * + * @returns Promise resolving to true if user approves changes, false otherwise + */ +export async function showToolsConfigConfirmationDialog( + mainWindow: BrowserWindow, + currentConfig: MCPToolsConfig, + nextConfig: MCPToolsConfig +): Promise { + const summary = summarizeMcpToolStateChanges(currentConfig, nextConfig); + if (summary.totalChanges === 0) { + return true; // No changes, allow operation + } + const result = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Apply Changes', 'Cancel'], + defaultId: 1, + title: 'MCP Tools Configuration Changes', + message: `${summary.totalChanges} tool configuration change(s) will be applied:`, + detail: summary.summaryText + '\n\nDo you want to apply these changes?', + }); + return result.response === 0; // 0 is "Apply Changes" +} diff --git a/app/electron/preload.ts b/app/electron/preload.ts index 4a32eb276a7..ee661feb18c 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -31,6 +31,7 @@ contextBridge.exposeInMainWorld('desktopApi', { 'request-backend-token', 'request-plugin-permission-secrets', 'request-backend-port', + 'cluster-changed', ]; if (validChannels.includes(channel)) { ipcRenderer.send(channel, data); @@ -60,4 +61,29 @@ contextBridge.exposeInMainWorld('desktopApi', { removeListener: (channel: string, func: (...args: unknown[]) => void) => { ipcRenderer.removeListener(channel, func); }, + + // @todo: move these to the send receive pattern above, restricted to ai-assistant only. + // @todo: do not enable if environment variable disabling mcp is set. + // MCP client APIs + mcp: { + executeTool: (toolName: string, args: Record, toolCallId?: string) => + ipcRenderer.invoke('mcp-execute-tool', { toolName, args, toolCallId }), + getStatus: () => ipcRenderer.invoke('mcp-get-status'), + resetClient: () => ipcRenderer.invoke('mcp-reset-client'), + getConfig: () => ipcRenderer.invoke('mcp-get-config'), + updateConfig: (config: any) => ipcRenderer.invoke('mcp-update-config', config), + getToolsConfig: () => ipcRenderer.invoke('mcp-get-tools-config'), + updateToolsConfig: (config: any) => ipcRenderer.invoke('mcp-update-tools-config', config), + setToolEnabled: (serverName: string, toolName: string, enabled: boolean) => + ipcRenderer.invoke('mcp-set-tool-enabled', { serverName, toolName, enabled }), + getToolStats: (serverName: string, toolName: string) => + ipcRenderer.invoke('mcp-get-tool-stats', { serverName, toolName }), + clusterChange: (cluster: string | null) => + ipcRenderer.invoke('mcp-cluster-change', { cluster }), + }, + + // Notify cluster change (for MCP server restart) + notifyClusterChange: (cluster: string | null) => { + ipcRenderer.send('cluster-changed', cluster); + }, }); diff --git a/app/electron/runCmd.ts b/app/electron/runCmd.ts index 88c498dd82e..20d60d95c06 100644 --- a/app/electron/runCmd.ts +++ b/app/electron/runCmd.ts @@ -15,13 +15,13 @@ */ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; -import { app, BrowserWindow, dialog } from 'electron'; +import { BrowserWindow, dialog } from 'electron'; import { IpcMainEvent } from 'electron/main'; import crypto from 'node:crypto'; -import fs from 'node:fs'; import path from 'path'; import i18n from './i18next.config'; import { defaultPluginsDir } from './plugin-management'; +import { loadSettings, saveSettings, SETTINGS_PATH } from './settings'; /** * Data sent from the renderer process when a 'run-command' event is emitted. @@ -65,30 +65,6 @@ function confirmCommandDialog(command: string, mainWindow: BrowserWindow): boole return resp === 0; } -const SETTINGS_PATH = path.join(app?.getPath('userData') || 'testing', 'settings.json'); - -/** - * Loads the user settings. - * If the settings file does not exist, an empty object is returned. - * @returns The settings object. - */ -function loadSettings(): Record { - try { - const data = fs.readFileSync(SETTINGS_PATH, 'utf-8'); - return JSON.parse(data); - } catch (error) { - return {}; - } -} - -/** - * Saves the user settings. - * @param settings - The settings object to save. - */ -function saveSettings(settings: Record) { - fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings), 'utf-8'); -} - /** * Checks if the user has already consented to running the command. * @@ -99,7 +75,7 @@ function saveSettings(settings: Record) { * @returns true if the user has consented to running the command, false otherwise. */ function checkCommandConsent(command: string, args: string[], mainWindow: BrowserWindow): boolean { - const settings = loadSettings(); + const settings = loadSettings(SETTINGS_PATH); const confirmedCommands = settings?.confirmedCommands; // Build the consent key: command + (first arg if present) @@ -121,7 +97,7 @@ function checkCommandConsent(command: string, args: string[], mainWindow: Browse settings.confirmedCommands = {}; } settings.confirmedCommands[consentKey] = commandChoice; - saveSettings(settings); + saveSettings(SETTINGS_PATH, settings); } return true; } @@ -150,7 +126,7 @@ const COMMANDS_WITH_CONSENT = { * @param pluginInfo artifacthub plugin info */ export function addRunCmdConsent(pluginInfo: { name: string }): void { - const settings = loadSettings(); + const settings = loadSettings(SETTINGS_PATH); if (!settings.confirmedCommands) { settings.confirmedCommands = {}; } @@ -169,7 +145,7 @@ export function addRunCmdConsent(pluginInfo: { name: string }): void { } } - saveSettings(settings); + saveSettings(SETTINGS_PATH, settings); } /** @@ -178,7 +154,7 @@ export function addRunCmdConsent(pluginInfo: { name: string }): void { * @param pluginName The package.json name of the plugin. */ export function removeRunCmdConsent(pluginName: string): void { - const settings = loadSettings(); + const settings = loadSettings(SETTINGS_PATH); if (!settings.confirmedCommands) { return; } @@ -193,7 +169,7 @@ export function removeRunCmdConsent(pluginName: string): void { delete settings.confirmedCommands[command]; } - saveSettings(settings); + saveSettings(SETTINGS_PATH, settings); } /** diff --git a/app/electron/settings.test.ts b/app/electron/settings.test.ts new file mode 100644 index 00000000000..eeaa332812e --- /dev/null +++ b/app/electron/settings.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'path'; +import { loadSettings, saveSettings } from './settings'; + +function tmpPath(): string { + return path.join(os.tmpdir(), `settings-test-${Date.now()}-${Math.random()}.json`); +} + +describe('settings load/save', () => { + let filePath: string; + + beforeEach(() => { + filePath = tmpPath(); + try { + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + } catch { + // ignore + } + }); + + afterEach(() => { + try { + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + } catch { + // ignore + } + }); + + it('loadSettings returns {} when file does not exist', () => { + expect(fs.existsSync(filePath)).toBe(false); + const res = loadSettings(filePath); + expect(res).toEqual({}); + }); + + it('saveSettings writes JSON and loadSettings reads it back', () => { + const obj = { a: 1, b: 'two', nested: { ok: true } }; + saveSettings(filePath, obj); + expect(fs.existsSync(filePath)).toBe(true); + + const raw = fs.readFileSync(filePath, 'utf-8'); + expect(raw).toBe(JSON.stringify(obj)); + + const loaded = loadSettings(filePath); + expect(loaded).toEqual(obj); + }); + + it('loadSettings returns {} for invalid JSON content', () => { + fs.writeFileSync(filePath, 'not-a-json', 'utf-8'); + const res = loadSettings(filePath); + expect(res).toEqual({}); + }); + + it('loadSettings returns {} when reading a directory path (read error)', () => { + // point to a directory to force read error + const dirPath = os.tmpdir(); + const res = loadSettings(dirPath); + expect(res).toEqual({}); + }); +}); diff --git a/app/electron/settings.ts b/app/electron/settings.ts new file mode 100644 index 00000000000..96768d0c63b --- /dev/null +++ b/app/electron/settings.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from 'electron'; +import fs from 'node:fs'; +import path from 'path'; + +export const SETTINGS_PATH = path.join(app?.getPath('userData') || 'testing', 'settings.json'); + +/** + * Loads the user settings. + * If the settings file does not exist, an empty object is returned. + * @returns The settings object. + */ +export function loadSettings(settingsPath: string): Record { + try { + const data = fs.readFileSync(settingsPath, 'utf-8'); + return JSON.parse(data); + } catch (error) { + return {}; + } +} + +/** + * Saves the user settings. + * @param settings - The settings object to save. + */ +export function saveSettings(settingsPath: string, settings: Record) { + fs.writeFileSync(settingsPath, JSON.stringify(settings), 'utf-8'); +} diff --git a/app/package-lock.json b/app/package-lock.json index c80fdd86ab4..519a04ef6c7 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,7 @@ "name": "headlamp", "version": "0.37.0", "dependencies": { + "@langchain/mcp-adapters": "^1.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", @@ -1958,6 +1959,13 @@ "hasInstallScript": true, "optional": true }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT", + "peer": true + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -3495,6 +3503,167 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/core": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.0.3.tgz", + "integrity": "sha512-3ABwsHfpvsDzNWZYAgPist5q+qjiU7lZDzEk7A9rXVhdhksPH8aFWnwFD6JhNP4wCgVqHWxvBaC1pj2gMa7iTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.64", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.0.1.tgz", + "integrity": "sha512-7y8OTDLrHrpJ55Y5x7c7zU2BbqNllXwxM106Xrd+NaQB5CpEb4hbUfIwe4XmhhscKPwvhXAq3tjeUxw9MCiurQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.0.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1", + "zod": "^3.25.32 || ^4.1.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "license": "MIT", + "peer": true, + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz", + "integrity": "sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/mcp-adapters": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-1.0.0.tgz", + "integrity": "sha512-bvoCF1F8iKsS4AaHPimhvzLX3Zpel7+nTSecUIDQHXQ6G1+rpvbx2zFJYRvhjLrbjvFneRN+HO0XAHtUuzWJdA==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.2", + "debug": "^4.4.3", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20.10.0" + }, + "optionalDependencies": { + "extended-eventsource": "^1.7.0" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0", + "@langchain/langgraph": "^1.0.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": false + }, + "@langchain/langgraph": { + "optional": false + } + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -3567,6 +3736,60 @@ "node": ">= 10.0.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz", + "integrity": "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@mswjs/interceptors": { "version": "0.38.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", @@ -3857,6 +4080,13 @@ "@types/node": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -3876,6 +4106,13 @@ "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", "dev": true }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT", + "peer": true + }, "node_modules/@types/verror": { "version": "1.10.9", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", @@ -4138,6 +4375,40 @@ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -4187,6 +4458,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -5031,7 +5341,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5099,6 +5408,26 @@ "bluebird": "^3.5.5" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5382,6 +5711,15 @@ "node": ">= 10.0.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5406,7 +5744,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5416,6 +5753,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5784,12 +6137,81 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "license": "MIT", + "peer": true, + "dependencies": { + "simple-wcswidth": "^1.1.2" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -5901,6 +6323,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -6124,11 +6559,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6139,6 +6575,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -6287,6 +6733,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6510,7 +6965,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6526,6 +6980,12 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6812,6 +7272,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -6939,7 +7408,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6949,7 +7417,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -7012,7 +7479,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7121,6 +7587,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -7697,6 +8169,43 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT", + "peer": true + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7757,6 +8266,91 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "license": "MIT", + "optional": true + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -7790,8 +8384,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-fifo": { "version": "1.3.2", @@ -7826,6 +8419,22 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -7905,6 +8514,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-process": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", @@ -8012,6 +8638,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -8182,7 +8826,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8216,7 +8859,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8420,7 +9062,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8506,7 +9147,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8625,6 +9265,31 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8803,7 +9468,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8931,6 +9595,15 @@ "node": ">= 0.10" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -9265,6 +9938,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -10224,6 +10903,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10347,6 +11036,42 @@ "node": ">=6" } }, + "node_modules/langsmith": { + "version": "0.3.78", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.78.tgz", + "integrity": "sha512-PVrog/DiTsiyOQ38GeZEIVadgk55/dfE3axagQksT3dt6KhFuRxhNaZrC0rp3dNW9RQJCm/c3tn+PiybwQNY0Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -10604,12 +11329,32 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -10828,9 +11573,20 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "peer": true, + "bin": { + "mustache": "bin/mustache" + } }, "node_modules/natural-compare": { "version": "1.4.0", @@ -10838,6 +11594,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -10955,17 +11720,15 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "peer": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -11085,6 +11848,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11180,6 +11955,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11264,6 +12093,15 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11323,6 +12161,16 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -11361,7 +12209,16 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, "engines": { - "node": ">= 6" + "node": ">= 6" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" } }, "node_modules/pkg-dir": { @@ -11534,6 +12391,19 @@ "node": ">= 8" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -11568,6 +12438,21 @@ } ] }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11649,6 +12534,46 @@ "rimraf": "bin.js" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rcedit": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", @@ -11915,6 +12840,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -12047,6 +12981,22 @@ "node": ">=8.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -12130,8 +13080,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sanitize-filename": { "version": "1.6.3", @@ -12149,9 +13098,10 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -12166,6 +13116,49 @@ "dev": true, "optional": true }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -12182,6 +13175,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12216,6 +13224,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12376,16 +13390,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "peer": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -12411,6 +13478,13 @@ "node": ">=10" } }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "license": "MIT", + "peer": true + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12513,6 +13587,15 @@ "node": ">= 6" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -13013,6 +14096,15 @@ "node": ">=10.13.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -13104,6 +14196,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -13289,6 +14416,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -13347,6 +14483,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -13370,6 +14520,15 @@ "node": ">= 10.13.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -13874,6 +15033,24 @@ "engines": { "node": ">= 6" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } }, "dependencies": { @@ -15209,6 +16386,12 @@ "integrity": "sha512-iTZ8cVGZ5dglNRyFdSj8U60mHIrC8XNIuOHN/NkM5/dQP4nsmpyqeQTAADLLQgoFCNJD+DiwQCv8dR2cCeWP4g==", "optional": true }, + "@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "peer": true + }, "@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -16205,6 +17388,89 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@langchain/core": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.0.3.tgz", + "integrity": "sha512-3ABwsHfpvsDzNWZYAgPist5q+qjiU7lZDzEk7A9rXVhdhksPH8aFWnwFD6JhNP4wCgVqHWxvBaC1pj2gMa7iTg==", + "peer": true, + "requires": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.64", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.76 || ^4" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "peer": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "peer": true + } + } + }, + "@langchain/langgraph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.0.1.tgz", + "integrity": "sha512-7y8OTDLrHrpJ55Y5x7c7zU2BbqNllXwxM106Xrd+NaQB5CpEb4hbUfIwe4XmhhscKPwvhXAq3tjeUxw9MCiurQ==", + "peer": true, + "requires": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.0.0", + "uuid": "^10.0.0" + } + }, + "@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "peer": true, + "requires": { + "uuid": "^10.0.0" + } + }, + "@langchain/langgraph-sdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz", + "integrity": "sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw==", + "peer": true, + "requires": { + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "peer": true + } + } + }, + "@langchain/mcp-adapters": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-1.0.0.tgz", + "integrity": "sha512-bvoCF1F8iKsS4AaHPimhvzLX3Zpel7+nTSecUIDQHXQ6G1+rpvbx2zFJYRvhjLrbjvFneRN+HO0XAHtUuzWJdA==", + "requires": { + "@modelcontextprotocol/sdk": "^1.18.2", + "debug": "^4.4.3", + "extended-eventsource": "^1.7.0", + "zod": "^3.25.76 || ^4" + } + }, "@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -16255,6 +17521,44 @@ } } }, + "@modelcontextprotocol/sdk": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz", + "integrity": "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==", + "requires": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "@mswjs/interceptors": { "version": "0.38.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", @@ -16529,6 +17833,12 @@ "@types/node": "*" } }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "peer": true + }, "@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -16547,6 +17857,12 @@ "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", "dev": true }, + "@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "peer": true + }, "@types/verror": { "version": "1.10.9", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", @@ -16716,6 +18032,30 @@ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true }, + "accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "requires": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -16750,6 +18090,32 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -17415,8 +18781,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "binary-extensions": { "version": "2.2.0", @@ -17466,6 +18831,22 @@ "bluebird": "^3.5.5" } }, + "body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "requires": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + } + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -17678,6 +19059,11 @@ "sax": "^1.2.4" } }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -17696,12 +19082,20 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -17971,12 +19365,51 @@ } } }, + "console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "peer": true, + "requires": { + "simple-wcswidth": "^1.1.2" + } + }, + "content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, "copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -18063,6 +19496,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -18216,13 +19658,19 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "peer": true + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -18329,6 +19777,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -18495,7 +19948,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -18507,6 +19959,11 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -18740,6 +20197,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -18849,14 +20311,12 @@ "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-get-iterator": { "version": "1.1.3", @@ -18912,7 +20372,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "requires": { "es-errors": "^1.3.0" } @@ -18996,6 +20455,11 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -19423,6 +20887,30 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + }, + "eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "requires": { + "eventsource-parser": "^3.0.1" + } + }, + "eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==" + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -19467,6 +20955,67 @@ "jest-util": "^29.7.0" } }, + "express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "requires": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, + "express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "requires": {} + }, + "extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "optional": true + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -19489,8 +21038,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-fifo": { "version": "1.3.2", @@ -19522,6 +21070,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" + }, "fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -19594,6 +21147,19 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "requires": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + } + }, "find-process": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", @@ -19677,6 +21243,16 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -19809,7 +21385,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -19833,7 +21408,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "requires": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -19973,8 +21547,7 @@ "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -20039,8 +21612,7 @@ "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", @@ -20136,6 +21708,25 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -20261,7 +21852,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -20343,6 +21933,11 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -20557,6 +22152,11 @@ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true }, + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -21272,6 +22872,15 @@ } } }, + "js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "peer": true, + "requires": { + "base64-js": "^1.5.1" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -21371,6 +22980,21 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "langsmith": { + "version": "0.3.78", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.78.tgz", + "integrity": "sha512-PVrog/DiTsiyOQ38GeZEIVadgk55/dfE3axagQksT3dt6KhFuRxhNaZrC0rp3dNW9RQJCm/c3tn+PiybwQNY0Q==", + "peer": true, + "requires": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + } + }, "language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -21573,8 +23197,17 @@ "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" }, "merge-stream": { "version": "2.0.0", @@ -21716,9 +23349,15 @@ "dev": true }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "peer": true }, "natural-compare": { "version": "1.4.0", @@ -21726,6 +23365,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -21828,15 +23472,12 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "peer": true + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "object-is": { "version": "1.1.6", @@ -21917,6 +23558,14 @@ "es-object-atoms": "^1.0.0" } }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -21987,6 +23636,43 @@ } } }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "peer": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "peer": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "peer": true + } + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "peer": true, + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -22047,6 +23733,11 @@ "parse5": "^7.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -22090,6 +23781,11 @@ } } }, + "path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -22119,6 +23815,11 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true }, + "pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==" + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -22249,6 +23950,15 @@ "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==" }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -22270,6 +23980,14 @@ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true }, + "qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "requires": { + "side-channel": "^1.1.0" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -22323,6 +24041,32 @@ } } }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "rcedit": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", @@ -22544,6 +24288,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -22640,6 +24389,18 @@ "sprintf-js": "^1.1.2" } }, + "router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "requires": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -22696,8 +24457,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sanitize-filename": { "version": "1.6.3", @@ -22715,9 +24475,9 @@ "dev": true }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" }, "semver-compare": { "version": "1.0.0", @@ -22726,6 +24486,39 @@ "dev": true, "optional": true }, + "send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "requires": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -22736,6 +24529,17 @@ "type-fest": "^0.13.1" } }, + "serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "requires": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + } + }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -22764,6 +24568,11 @@ "has-property-descriptors": "^1.0.2" } }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -22878,16 +24687,47 @@ } }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "peer": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "requires": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "signal-exit": { @@ -22904,6 +24744,12 @@ "semver": "^7.5.3" } }, + "simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "peer": true + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -22981,6 +24827,11 @@ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -23380,6 +25231,11 @@ "streamx": "^2.12.5" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -23450,6 +25306,31 @@ "dev": true, "optional": true }, + "type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "requires": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -23581,6 +25462,11 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, "untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -23616,6 +25502,12 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "peer": true + }, "v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -23633,6 +25525,11 @@ "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, "verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -24019,6 +25916,17 @@ } } } + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + }, + "zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "requires": {} } } } diff --git a/app/package.json b/app/package.json index d4f111ac8aa..87084a45bd7 100644 --- a/app/package.json +++ b/app/package.json @@ -123,7 +123,11 @@ "electron/windowSize.js", "electron/env-paths.js", "electron/plugin-management.js", - "electron/runCmd.js" + "electron/runCmd.js", + "electron/mcp/MCPClient.js", + "electron/mcp/MCPToolStateStore.js", + "electron/settings.js", + "electron/mcp/MCPSettings.js" ], "extraResources": [ { @@ -171,6 +175,7 @@ "typescript": "5.5.4" }, "dependencies": { + "@langchain/mcp-adapters": "^1.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5",