diff --git a/resources/wasm/qjs-wasi.wasm b/resources/wasm/qjs-wasi.wasm new file mode 100644 index 0000000000..ca528040c5 Binary files /dev/null and b/resources/wasm/qjs-wasi.wasm differ diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts index 46d3bb87d2..410df57c75 100644 --- a/src/main/mcpServers/factory.ts +++ b/src/main/mcpServers/factory.ts @@ -6,6 +6,7 @@ import BraveSearchServer from './brave-search' import DifyKnowledgeServer from './dify-knowledge' import FetchServer from './fetch' import FileSystemServer from './filesystem' +import JsServer from './js' import MemoryServer from './memory' import PythonServer from './python' import ThinkingServer from './sequentialthinking' @@ -42,6 +43,9 @@ export function createInMemoryMCPServer( case BuiltinMCPServerNames.python: { return new PythonServer().server } + case BuiltinMCPServerNames.js: { + return new JsServer().server + } default: throw new Error(`Unknown in-memory MCP server: ${name}`) } diff --git a/src/main/mcpServers/js.ts b/src/main/mcpServers/js.ts new file mode 100644 index 0000000000..b83b16da48 --- /dev/null +++ b/src/main/mcpServers/js.ts @@ -0,0 +1,139 @@ +// port from https://github.com/jlucaso1/mcp-javascript-sandbox +import { loggerService } from '@logger' +import { jsService } from '@main/services/JsService' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types' +import { z } from 'zod' + +const TOOL_NAME = 'run_javascript_code' +const DEFAULT_TIMEOUT = 60_000 + +export const RequestPayloadSchema = z.object({ + javascript_code: z.string().min(1).describe('The JavaScript code to execute in the sandbox.'), + timeout: z + .number() + .int() + .positive() + .max(5 * 60_000) + .optional() + .describe('Execution timeout in milliseconds (default 60000, max 300000).') +}) + +const logger = loggerService.withContext('MCPServer:JavaScript') + +function formatExecutionResult(result: { + stdout: string + stderr: string + error?: string | undefined + exitCode: number +}) { + let combinedOutput = '' + if (result.stdout) { + combinedOutput += result.stdout + } + if (result.stderr) { + combinedOutput += `--- stderr ---\n${result.stderr}\n--- stderr ---\n` + } + if (result.error) { + combinedOutput += `--- Execution Error ---\n${result.error}\n--- Execution Error ---\n` + } + + const isError = Boolean(result.error) || Boolean(result.stderr?.trim()) || result.exitCode !== 0 + + return { + combinedOutput: combinedOutput.trim(), + isError + } +} + +class JsServer { + public server: Server + + constructor() { + this.server = new Server( + { + name: 'MCP QuickJS Runner', + version: '1.0.0', + description: 'An MCP server that provides a tool to execute JavaScript code in a QuickJS WASM sandbox.' + }, + { + capabilities: { + resources: {}, + tools: {} + } + } + ) + + this.setupHandlers() + } + + private setupHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: TOOL_NAME, + description: + 'Executes the provided JavaScript code in a secure WASM sandbox (QuickJS). Returns stdout and stderr. Non-zero exit code indicates an error.', + inputSchema: z.toJSONSchema(RequestPayloadSchema) + } + ] + } + }) + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + if (name !== TOOL_NAME) { + return { + content: [{ type: 'text', text: `Tool not found: ${name}` }], + isError: true + } + } + + const parseResult = RequestPayloadSchema.safeParse(args) + if (!parseResult.success) { + return { + content: [{ type: 'text', text: `Invalid arguments: ${parseResult.error.message}` }], + isError: true + } + } + + const { javascript_code, timeout } = parseResult.data + + try { + logger.debug('Executing JavaScript code via JsService') + const result = await jsService.executeScript(javascript_code, { + timeout: timeout ?? DEFAULT_TIMEOUT + }) + + const { combinedOutput, isError } = formatExecutionResult(result as any) + + return { + content: [ + { + type: 'text', + text: combinedOutput + } + ], + isError + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logger.error(`JavaScript execution failed: ${message}`) + + return { + content: [ + { + type: 'text', + text: `Server error during tool execution: ${message}` + } + ], + isError: true + } + } + }) + } +} + +export default JsServer diff --git a/src/main/services/JsService.ts b/src/main/services/JsService.ts new file mode 100644 index 0000000000..9435fceeb0 --- /dev/null +++ b/src/main/services/JsService.ts @@ -0,0 +1,107 @@ +import { loggerService } from '@logger' + +import type { JsExecutionResult } from './workers/JsWorker' +// oxlint-disable-next-line default +import createJsWorker from './workers/JsWorker?nodeWorker' + +interface ExecuteScriptOptions { + timeout?: number +} + +type WorkerResponse = + | { + success: true + result: JsExecutionResult + } + | { + success: false + error: string + } + +const DEFAULT_TIMEOUT = 60_000 + +const logger = loggerService.withContext('JsService') + +export class JsService { + private static instance: JsService | null = null + + private constructor() {} + + public static getInstance(): JsService { + if (!JsService.instance) { + JsService.instance = new JsService() + } + return JsService.instance + } + + public async executeScript(code: string, options: ExecuteScriptOptions = {}): Promise { + const { timeout = DEFAULT_TIMEOUT } = options + + if (!code || typeof code !== 'string') { + throw new Error('JavaScript code must be a non-empty string') + } + + return new Promise((resolve, reject) => { + const worker = createJsWorker({ + workerData: { code }, + argv: [], + trackUnmanagedFds: false + }) + + let settled = false + let timeoutId: NodeJS.Timeout | null = null + + const cleanup = async () => { + if (timeoutId) { + clearTimeout(timeoutId) + timeoutId = null + } + try { + await worker.terminate() + } catch { + // ignore termination errors + } + } + + const settleSuccess = async (result: JsExecutionResult) => { + if (settled) return + settled = true + await cleanup() + resolve(result) + } + + const settleError = async (error: Error) => { + if (settled) return + settled = true + await cleanup() + reject(error) + } + + worker.once('message', async (message: WorkerResponse) => { + if (message.success) { + await settleSuccess(message.result) + } else { + await settleError(new Error(message.error)) + } + }) + + worker.once('error', async (error) => { + logger.error(`JsWorker error: ${error instanceof Error ? error.message : String(error)}`) + await settleError(error instanceof Error ? error : new Error(String(error))) + }) + + worker.once('exit', async (exitCode) => { + if (!settled && exitCode !== 0) { + await settleError(new Error(`JsWorker exited with code ${exitCode}`)) + } + }) + + timeoutId = setTimeout(() => { + logger.warn(`JavaScript execution timed out after ${timeout}ms`) + void settleError(new Error('JavaScript execution timed out')) + }, timeout) + }) + } +} + +export const jsService = JsService.getInstance() diff --git a/src/main/services/workers/JsWorker.ts b/src/main/services/workers/JsWorker.ts new file mode 100644 index 0000000000..a6acdb68e9 --- /dev/null +++ b/src/main/services/workers/JsWorker.ts @@ -0,0 +1,115 @@ +import { mkdtemp, open, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { env } from 'node:process' +import { WASI } from 'node:wasi' +import { parentPort, workerData } from 'node:worker_threads' + +import loadWasm from '../../../../resources/wasm/qjs-wasi.wasm?loader' + +interface WorkerPayload { + code: string +} + +export interface JsExecutionResult { + stdout: string + stderr: string + error?: string + exitCode: number +} + +if (!parentPort) { + throw new Error('JsWorker requires a parent port') +} + +async function runQuickJsInSandbox(jsCode: string): Promise { + let tempDir: string | undefined + let stdoutHandle: Awaited> | undefined + let stderrHandle: Awaited> | undefined + let stdoutPath: string | undefined + let stderrPath: string | undefined + + try { + tempDir = await mkdtemp(join(tmpdir(), 'quickjs-wasi-')) + stdoutPath = join(tempDir, 'stdout.log') + stderrPath = join(tempDir, 'stderr.log') + + stdoutHandle = await open(stdoutPath, 'w') + stderrHandle = await open(stderrPath, 'w') + + const wasi = new WASI({ + version: 'preview1', + args: ['qjs', '-e', jsCode], + env, + stdin: 0, + stdout: stdoutHandle.fd, + stderr: stderrHandle.fd, + returnOnExit: true + }) + const instance = await loadWasm(wasi.getImportObject() as WebAssembly.Imports) + + let exitCode = 0 + try { + exitCode = wasi.start(instance) + } catch (wasiError: any) { + return { + stdout: '', + stderr: `WASI start error: ${wasiError?.message ?? String(wasiError)}`, + error: `Sandbox execution failed during start: ${wasiError?.message ?? String(wasiError)}`, + exitCode: -1 + } + } + + await stdoutHandle.close() + stdoutHandle = undefined + await stderrHandle.close() + stderrHandle = undefined + + const capturedStdout = await readFile(stdoutPath, 'utf8') + const capturedStderr = await readFile(stderrPath, 'utf8') + + let executionError: string | undefined + if (exitCode !== 0) { + executionError = `QuickJS process exited with code ${exitCode}. Check stderr for details.` + } + + return { + stdout: capturedStdout, + stderr: capturedStderr, + error: executionError, + exitCode + } + } catch (error: any) { + return { + stdout: '', + stderr: '', + error: `Sandbox setup or execution failed: ${error?.message ?? String(error)}`, + exitCode: -1 + } + } finally { + if (stdoutHandle) await stdoutHandle.close() + if (stderrHandle) await stderrHandle.close() + if (tempDir) { + await rm(tempDir, { recursive: true, force: true }) + } + } +} + +async function execute(code: string) { + return runQuickJsInSandbox(code) +} + +const payload = workerData as WorkerPayload | undefined + +if (!payload?.code || typeof payload.code !== 'string') { + parentPort.postMessage({ success: false, error: 'JavaScript code must be provided to the worker' }) +} else { + execute(payload.code) + .then((result) => { + parentPort?.postMessage({ success: true, result }) + }) + .catch((error: any) => { + const errorMessage = error instanceof Error ? error.message : String(error) + parentPort?.postMessage({ success: false, error: errorMessage }) + }) +} diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index ad3c326d9f..b9287257b6 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -322,7 +322,8 @@ const builtInMcpDescriptionKeyMap: Record = { [BuiltinMCPServerNames.fetch]: 'settings.mcp.builtinServersDescriptions.fetch', [BuiltinMCPServerNames.filesystem]: 'settings.mcp.builtinServersDescriptions.filesystem', [BuiltinMCPServerNames.difyKnowledge]: 'settings.mcp.builtinServersDescriptions.dify_knowledge', - [BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python' + [BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python', + [BuiltinMCPServerNames.js]: 'settings.mcp.builtinServersDescriptions.js' } as const export const getBuiltInMcpServerDescriptionLabel = (key: string): string => { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0f3d2a3f24..593f9d6965 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3346,6 +3346,7 @@ "dify_knowledge": "Dify's MCP server implementation provides a simple API to interact with Dify. Requires configuring the Dify Key", "fetch": "MCP server for retrieving URL web content", "filesystem": "A Node.js server implementing the Model Context Protocol (MCP) for file system operations. Requires configuration of directories allowed for access.", + "js": "Execute JavaScript code in a secure sandbox environment. Supports most standard libraries.", "mcp_auto_install": "Automatically install MCP service (beta)", "memory": "Persistent memory implementation based on a local knowledge graph. This enables the model to remember user-related information across different conversations. Requires configuring the MEMORY_FILE_PATH environment variable.", "no": "No description", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ce2dc5c222..91c30bd4a1 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3346,6 +3346,7 @@ "dify_knowledge": "Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互。需要配置 Dify Key", "fetch": "用于获取 URL 网页内容的 MCP 服务器", "filesystem": "实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器。需要配置允许访问的目录", + "js": "在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript,支持大多数标准库和流行的第三方库", "mcp_auto_install": "自动安装 MCP 服务(测试版)", "memory": "基于本地知识图谱的持久性记忆基础实现。这使得模型能够在不同对话间记住用户的相关信息。需要配置 MEMORY_FILE_PATH 环境变量。", "no": "无描述", diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx index 11f29221d6..2d2de37b8e 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx @@ -535,7 +535,30 @@ const CollapsedContent: FC<{ isExpanded: boolean; resultString: string }> = ({ i } const highlight = async () => { - const result = await highlightCode(resultString, 'json') + // 处理转义字符 + let processedString = resultString + try { + // 尝试解析字符串以处理可能的转义 + const parsed = JSON.parse(resultString) + if (typeof parsed === 'string') { + // 如果解析后是字符串,再次尝试解析(处理双重转义) + try { + const doubleParsed = JSON.parse(parsed) + processedString = JSON.stringify(doubleParsed, null, 2) + } catch { + // 不是有效的 JSON,使用解析后的字符串 + processedString = parsed + } + } else { + // 重新格式化 JSON + processedString = JSON.stringify(parsed, null, 2) + } + } catch { + // 解析失败,使用原始字符串 + processedString = resultString + } + + const result = await highlightCode(processedString, 'json') setStyledResult(result) } diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 2bae82c147..949b595cd1 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -144,6 +144,13 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ type: 'inMemory', isActive: false, provider: 'CherryAI' + }, + { + id: nanoid(), + name: BuiltinMCPServerNames.js, + type: 'inMemory', + isActive: false, + provider: 'CherryAI' } ] as const diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 3f38b57749..98244a3969 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -833,7 +833,8 @@ export const BuiltinMCPServerNames = { fetch: '@cherry/fetch', filesystem: '@cherry/filesystem', difyKnowledge: '@cherry/dify-knowledge', - python: '@cherry/python' + python: '@cherry/python', + js: '@cherry/js' } as const export type BuiltinMCPServerName = (typeof BuiltinMCPServerNames)[keyof typeof BuiltinMCPServerNames] diff --git a/yarn.lock b/yarn.lock index 3309a1c5f5..67fd71fd6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13440,6 +13440,7 @@ __metadata: unified: "npm:^11.0.5" uuid: "npm:^10.0.0" vite: "npm:rolldown-vite@latest" + vite-plugin-static-copy: "npm:^3.1.3" vitest: "npm:^3.2.4" webdav: "npm:^5.8.0" winston: "npm:^3.17.0" @@ -13804,6 +13805,16 @@ __metadata: languageName: node linkType: hard +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + "app-builder-bin@npm:5.0.0-alpha.12": version: 5.0.0-alpha.12 resolution: "app-builder-bin@npm:5.0.0-alpha.12" @@ -14203,7 +14214,7 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^2.2.0": +"binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 @@ -14316,7 +14327,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -14838,6 +14849,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^3.6.0": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + "chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" @@ -18301,6 +18331,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.3.2": + version: 11.3.2 + resolution: "fs-extra@npm:11.3.2" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/f5d629e1bb646d5dedb4d8b24c5aad3deb8cc1d5438979d6f237146cd10e113b49a949ae1b54212c2fbc98e2d0995f38009a9a1d0520f0287943335e65fe919b + languageName: node + linkType: hard + "fs-extra@npm:^8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -18359,7 +18400,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.3.3": +"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -18378,7 +18419,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -18543,7 +18584,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2": +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -19501,6 +19542,15 @@ __metadata: languageName: node linkType: hard +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + "is-buffer@npm:^2.0.0": version: 2.0.5 resolution: "is-buffer@npm:2.0.5" @@ -19588,7 +19638,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -22766,7 +22816,7 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0": +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 @@ -23252,7 +23302,7 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^7.0.2": +"p-map@npm:^7.0.2, p-map@npm:^7.0.3": version: 7.0.3 resolution: "p-map@npm:7.0.3" checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c @@ -23619,7 +23669,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -25219,6 +25269,15 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -27313,6 +27372,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 + languageName: node + linkType: hard + "tinypool@npm:^1.1.1": version: 1.1.1 resolution: "tinypool@npm:1.1.1" @@ -28333,6 +28402,21 @@ __metadata: languageName: node linkType: hard +"vite-plugin-static-copy@npm:^3.1.3": + version: 3.1.3 + resolution: "vite-plugin-static-copy@npm:3.1.3" + dependencies: + chokidar: "npm:^3.6.0" + fs-extra: "npm:^11.3.2" + p-map: "npm:^7.0.3" + picocolors: "npm:^1.1.1" + tinyglobby: "npm:^0.2.15" + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10c0/f58bf609246c440b4e3c0db10abf5965658c34ee03e72b94d4fc6ff35fa4568b5baa0fe36057234a4b1e84a9b4b3c2cdbff9f943b9e69d883d3a05353cbf9090 + languageName: node + linkType: hard + "vite@npm:rolldown-vite@latest": version: 7.1.5 resolution: "rolldown-vite@npm:7.1.5"