From 8f656cc6f7acac5c1db9e61d30bfcf4ffab694d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E7=83=A8?= <28805930+Guyungy@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:53:54 +0800 Subject: [PATCH] Add embedded MCP server with shared API facade --- README.md | 50 +- electron/mcp.ts | 45 + electron/services/chatService.ts | 12 +- electron/services/config.ts | 12 +- electron/services/httpApiFacade.ts | 929 ++++++++++++++++ electron/services/imageDecryptService.ts | 11 +- electron/services/mcp/server.ts | 163 +++ electron/services/runtimePaths.ts | 78 ++ electron/services/videoService.ts | 8 +- package-lock.json | 1228 +++++++++++++++------- package.json | 4 + scripts/mcp-runner.js | 41 + src/components/ai/AISummarySettings.tsx | 57 + src/pages/SettingsPage.tsx | 19 + src/services/config.ts | 22 + vite.config.ts | 9 + 16 files changed, 2311 insertions(+), 377 deletions(-) create mode 100644 electron/mcp.ts create mode 100644 electron/services/httpApiFacade.ts create mode 100644 electron/services/mcp/server.ts create mode 100644 electron/services/runtimePaths.ts create mode 100644 scripts/mcp-runner.js diff --git a/README.md b/README.md index 064068e..c9856f4 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,54 @@ npm run build:core --- +## MCP Server + +CipherTalk 现已提供基于 `stdio` 的内嵌 MCP Server,可供 Claude Desktop、Codex、Cherry Studio 等 MCP 宿主直接读取本地聊天数据。 + +### 启动 + +```bash +npm run build:mcp +node scripts/mcp-runner.js +``` + +### 首批工具 + +- `health_check` +- `get_status` +- `list_sessions` +- `get_messages` +- `list_contacts` + +### 宿主配置示例 + +```json +{ + "mcpServers": { + "ciphertalk": { + "command": "node", + "args": ["scripts/mcp-runner.js"], + "cwd": "E:/CipherTalk" + } + } +} +``` + +### 参数示例 + +```json +{ + "name": "get_messages", + "arguments": { + "sessionId": "wxid_xxx", + "limit": 20, + "fields": ["base", "time", "sender", "media"] + } +} +``` + +--- + ## 💻 开发指南 ### 代码规范 @@ -351,4 +399,4 @@ export const useChatStore = create((set) => ({ 一鲸落,万物生 · 愿每一段对话都被温柔以待 ❤️ by the CipherTalk Team - \ No newline at end of file + diff --git a/electron/mcp.ts b/electron/mcp.ts new file mode 100644 index 0000000..8db78cb --- /dev/null +++ b/electron/mcp.ts @@ -0,0 +1,45 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { createCipherTalkMcpServer } from './services/mcp/server' + +let mcpServer: Awaited> | null = null + +async function start() { + mcpServer = createCipherTalkMcpServer() + const transport = new StdioServerTransport() + await mcpServer.connect(transport) + + process.stderr.write('[CipherTalk MCP] stdio server started\n') +} + +async function shutdown(code = 0) { + try { + await mcpServer?.close() + } catch (error) { + process.stderr.write(`[CipherTalk MCP] close error: ${String(error)}\n`) + } finally { + process.exit(code) + } +} + +process.on('SIGINT', () => { + void shutdown(0) +}) + +process.on('SIGTERM', () => { + void shutdown(0) +}) + +process.on('uncaughtException', (error) => { + process.stderr.write(`[CipherTalk MCP] uncaughtException: ${String(error)}\n`) + void shutdown(1) +}) + +process.on('unhandledRejection', (error) => { + process.stderr.write(`[CipherTalk MCP] unhandledRejection: ${String(error)}\n`) + void shutdown(1) +}) + +void start().catch((error) => { + process.stderr.write(`[CipherTalk MCP] startup failed: ${String(error)}\n`) + void shutdown(1) +}) diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 0e7a64d..31acd34 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -5,8 +5,8 @@ import * as path from 'path' import * as https from 'https' import * as http from 'http' import * as fzstd from 'fzstd' -import { app } from 'electron' import { ConfigService } from './config' +import { getAppPath, getDocumentsPath, getExePath, isElectronPackaged } from './runtimePaths' export interface ChatSession { username: string @@ -258,19 +258,19 @@ class ChatService extends EventEmitter { // 开发环境使用文档目录 if (process.env.VITE_DEV_SERVER_URL) { - const documentsPath = app.getPath('documents') + const documentsPath = getDocumentsPath() return path.join(documentsPath, 'CipherTalkData') } // 生产环境 - const exePath = app.getPath('exe') + const exePath = getExePath() const installDir = path.dirname(exePath) // 检查是否安装在 C 盘 const isOnCDrive = /^[cC]:/i.test(installDir) || installDir.startsWith('\\') if (isOnCDrive) { - const documentsPath = app.getPath('documents') + const documentsPath = getDocumentsPath() return path.join(documentsPath, 'CipherTalkData') } @@ -4645,7 +4645,7 @@ class ChatService extends EventEmitter { // 找到 silk-wasm 的 WASM 文件 let wasmPath: string - if (app.isPackaged) { + if (isElectronPackaged()) { // 打包后,WASM 文件在 app.asar.unpacked 中 wasmPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') if (!fs.existsSync(wasmPath)) { @@ -4653,7 +4653,7 @@ class ChatService extends EventEmitter { } } else { // 开发环境 - wasmPath = path.join(app.getAppPath(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') + wasmPath = path.join(getAppPath(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') } if (!fs.existsSync(wasmPath)) { diff --git a/electron/services/config.ts b/electron/services/config.ts index 9160fbe..b86d204 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -1,7 +1,7 @@ import Database from 'better-sqlite3' -import { app } from 'electron' import path from 'path' import fs from 'fs' +import { getUserDataPath } from './runtimePaths' interface ConfigSchema { // 数据库相关 @@ -77,6 +77,8 @@ interface ConfigSchema { aiEnableCache: boolean aiEnableThinking: boolean // 是否显示思考过程 aiMessageLimit: number // 摘要提取的消息条数限制 + mcpEnabled: boolean + mcpExposeMediaPaths: boolean } const defaults: ConfigSchema = { @@ -120,7 +122,9 @@ const defaults: ConfigSchema = { aiCustomSystemPrompt: '', aiEnableCache: true, aiEnableThinking: true, // 默认显示思考过程 - aiMessageLimit: 3000 // 默认3000条,用户可调至5000 + aiMessageLimit: 3000, // 默认3000条,用户可调至5000 + mcpEnabled: false, + mcpExposeMediaPaths: true } export class ConfigService { @@ -128,7 +132,7 @@ export class ConfigService { private dbPath: string constructor() { - const userDataPath = app.getPath('userData') + const userDataPath = getUserDataPath() this.dbPath = path.join(userDataPath, 'ciphertalk-config.db') this.initDatabase() } @@ -389,6 +393,6 @@ export class ConfigService { if (configured && configured.trim().length > 0) { return configured } - return path.join(app.getPath('documents'), 'CipherTalk') + return path.join(getUserDataPath(), 'CipherTalk') } } diff --git a/electron/services/httpApiFacade.ts b/electron/services/httpApiFacade.ts new file mode 100644 index 0000000..4d1ca9a --- /dev/null +++ b/electron/services/httpApiFacade.ts @@ -0,0 +1,929 @@ +import { existsSync, mkdirSync } from 'fs' +import { writeFile } from 'fs/promises' +import { fileURLToPath } from 'url' +import { join } from 'path' +import { ConfigService } from './config' +import { chatService } from './chatService' +import { imageDecryptService } from './imageDecryptService' +import { videoService } from './videoService' +import { getAppVersion } from './runtimePaths' + +type ContactType = 'friend' | 'group' | 'official' | 'former_friend' | 'other' +type SessionTypeFilter = 'friend' | 'group' | 'official' | 'other' + +export interface HttpApiRuntimeStatus { + enabled: boolean + running: boolean + host: string + port: number + startedAt: number + token: string + startError: string +} + +export interface ApiErrorShape { + code: string + message: string + hint?: string +} + +export class ApiQueryError extends Error { + statusCode: number + code: string + hint?: string + + constructor(statusCode: number, code: string, message: string, hint?: string) { + super(message) + this.name = 'ApiQueryError' + this.statusCode = statusCode + this.code = code + this.hint = hint + } + + toResponse(): ApiErrorShape { + return { + code: this.code, + message: this.message, + hint: this.hint + } + } +} + +export interface QuerySessionsInput { + q?: string + type?: string[] | null + unreadOnly?: boolean + sort?: string + offset?: number + limit?: number +} + +export interface QueryMessagesInput { + sessionId: string + offset?: number + limit?: number + sort?: string + keyword?: string + msgType?: number[] | null + messageKind?: string[] | null + appMsgType?: string[] | null + startTime?: number | null + endTime?: number | null + includeRaw?: boolean + resolveMediaPath?: boolean + resolveVoicePath?: boolean + adaptive?: boolean + maxScan?: number + fields?: string[] | null +} + +export interface QueryContactsInput { + q?: string + type?: string[] | null + includeAvatar?: boolean + sort?: string + offset?: number + limit?: number +} + +function parseBoolean(value: string | null | undefined, defaultValue: boolean): boolean { + if (value == null) return defaultValue + const normalized = value.trim().toLowerCase() + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true + if (['0', 'false', 'no', 'off'].includes(normalized)) return false + return defaultValue +} + +function parseIntInRange(value: string | number | null | undefined, defaultValue: number, min: number, max: number): number { + if (value == null || value === '') return defaultValue + const n = typeof value === 'number' ? value : Number.parseInt(value, 10) + if (!Number.isFinite(n)) return defaultValue + return Math.max(min, Math.min(max, n)) +} + +function parseNumberList(value: string | null | undefined): number[] | null { + if (!value) return null + const nums = value + .split(',') + .map((x) => Number.parseInt(x.trim(), 10)) + .filter((x) => Number.isFinite(x)) + return nums.length > 0 ? nums : null +} + +function parseStringSet(value: string | null | undefined): Set | null { + if (!value) return null + const values = value + .split(',') + .map((x) => x.trim().toLowerCase()) + .filter(Boolean) + return values.length > 0 ? new Set(values) : null +} + +function parseFields(value: string | null | undefined): Set { + const defaults = ['base', 'type', 'time', 'sender', 'metadata', 'media'] + if (!value || !value.trim()) { + return new Set(defaults) + } + + const parts = value + .split(',') + .map((x) => x.trim().toLowerCase()) + .filter(Boolean) + + if (parts.includes('all')) { + return new Set([ + 'all', + 'base', + 'type', + 'time', + 'sender', + 'metadata', + 'media', + 'quote', + 'file', + 'transfer', + 'chatrecord', + 'raw', + 'schema' + ]) + } + + return new Set(parts) +} + +function parseTimestampMs(value: string | number | null | undefined): number | null { + if (value == null || value === '') return null + const raw = typeof value === 'number' ? value : Number.parseInt(value, 10) + if (!Number.isFinite(raw) || raw <= 0) return null + return raw < 1_000_000_000_000 ? raw * 1000 : raw +} + +function normalizeTimestampMs(value: number): number { + if (!Number.isFinite(value) || value <= 0) return 0 + return value < 1_000_000_000_000 ? value * 1000 : value +} + +function extractXmlType(content?: string): string | undefined { + if (!content) return undefined + const match = content.match(/\s*([^<]+)\s*<\/type>/i) + return match?.[1]?.trim() +} + +function fileUrlToPathMaybe(input?: string | null): string | null { + if (!input) return null + if (input.startsWith('file:///')) { + try { + return fileURLToPath(input) + } catch { + return null + } + } + return input +} + +function sanitizePathPart(value: string): string { + return value.replace(/[\\/:*?"<>|]/g, '_') +} + +function pruneEmpty(value: any): any { + if (value === null || value === undefined) return undefined + if (typeof value === 'string') return value === '' ? undefined : value + if (Array.isArray(value)) { + const next = value + .map((v) => pruneEmpty(v)) + .filter((v) => v !== undefined) + return next.length > 0 ? next : undefined + } + if (typeof value === 'object') { + const out: Record = {} + for (const [k, v] of Object.entries(value)) { + const pruned = pruneEmpty(v) + if (pruned !== undefined) out[k] = pruned + } + return Object.keys(out).length > 0 ? out : undefined + } + return value +} + +function detectMessageKind(message: Record): { + messageKind: string + typeLabel: string + appMsgType?: string +} { + const localType = Number(message.localType || 0) + const raw = String(message.rawContent || message.parsedContent || '') + const appMsgType = extractXmlType(raw) + + if (localType === 1) return { messageKind: 'text', typeLabel: '文本' } + if (localType === 3) return { messageKind: 'image', typeLabel: '图片' } + if (localType === 34) return { messageKind: 'voice', typeLabel: '语音' } + if (localType === 42) return { messageKind: 'contact_card', typeLabel: '名片' } + if (localType === 43) return { messageKind: 'video', typeLabel: '视频' } + if (localType === 47) return { messageKind: 'emoji', typeLabel: '表情' } + if (localType === 48) return { messageKind: 'location', typeLabel: '位置' } + if (localType === 50) return { messageKind: 'voip', typeLabel: '音视频通话' } + if (localType === 10000) return { messageKind: 'system', typeLabel: '系统消息' } + if (localType === 244813135921) return { messageKind: 'quote', typeLabel: '引用消息' } + + if (localType === 49 || appMsgType) { + switch (appMsgType) { + case '3': + return { messageKind: 'app_music', typeLabel: '音乐分享', appMsgType } + case '5': + case '49': + return { messageKind: 'app_link', typeLabel: '链接', appMsgType } + case '6': + return { messageKind: 'app_file', typeLabel: '文件', appMsgType } + case '19': + return { messageKind: 'app_chat_record', typeLabel: '聊天记录', appMsgType } + case '33': + case '36': + return { messageKind: 'app_mini_program', typeLabel: '小程序', appMsgType } + case '57': + return { messageKind: 'app_quote', typeLabel: '引用消息', appMsgType } + case '62': + return { messageKind: 'app_pat', typeLabel: '拍一拍', appMsgType } + case '87': + return { messageKind: 'app_announcement', typeLabel: '群公告', appMsgType } + case '115': + return { messageKind: 'app_gift', typeLabel: '微信礼物', appMsgType } + case '2000': + return { messageKind: 'app_transfer', typeLabel: '转账', appMsgType } + case '2001': + return { messageKind: 'app_red_packet', typeLabel: '红包', appMsgType } + default: + return { messageKind: 'app', typeLabel: '应用消息', appMsgType } + } + } + + return { messageKind: 'unknown', typeLabel: `未知类型(${localType})` } +} + +function parseTypeFilter(values?: string[] | null): Set | null { + if (!values?.length) return null + const allowed: ContactType[] = ['friend', 'group', 'official', 'former_friend', 'other'] + const result = new Set() + values.forEach((x) => { + if (allowed.includes(x as ContactType)) { + result.add(x as ContactType) + } + }) + return result.size > 0 ? result : null +} + +function parseSessionTypeFilter(values?: string[] | null): Set | null { + if (!values?.length) return null + const allowed: SessionTypeFilter[] = ['friend', 'group', 'official', 'other'] + const result = new Set() + values.forEach((x) => { + if (allowed.includes(x as SessionTypeFilter)) { + result.add(x as SessionTypeFilter) + } + }) + return result.size > 0 ? result : null +} + +function detectSessionType(username: string): SessionTypeFilter { + if (username.includes('@chatroom')) return 'group' + if (username.startsWith('gh_')) return 'official' + if (username) return 'friend' + return 'other' +} + +function getBaseUrl(status: HttpApiRuntimeStatus): string { + return `http://${status.host}:${status.port}/v1` +} + +export function queryHealth() { + return { status: 'ok' } +} + +export function queryStatus(status: HttpApiRuntimeStatus, verbose = false) { + const configService = new ConfigService() + const hasDbPath = Boolean(configService.get('dbPath')) + const hasWxid = Boolean(configService.get('myWxid')) + const hasDecryptKey = Boolean(configService.get('decryptKey')) + configService.close() + + const isApiEnabled = status.enabled + const isApiRunning = status.running + const isDbConfigReady = hasDbPath && hasWxid && hasDecryptKey + + let state: 'ready' | 'disabled' | 'starting_or_error' | 'needs_config' = 'ready' + let message = 'HTTP API is ready for external calls.' + + if (!isApiEnabled) { + state = 'disabled' + message = 'HTTP API is disabled. Enable it in Settings > Open API.' + } else if (!isApiRunning) { + state = 'starting_or_error' + message = status.startError || 'HTTP API is enabled but not running. Try restart in settings.' + } else if (!isDbConfigReady) { + state = 'needs_config' + message = 'API is running, but database-related features need dbPath/decryptKey/wxid configuration.' + } + + const basePayload = { + summary: { + state, + usable: isApiEnabled && isApiRunning, + message + }, + server: { + running: isApiRunning, + enabled: isApiEnabled, + host: status.host, + port: status.port, + uptimeMs: status.startedAt ? Date.now() - status.startedAt : 0 + }, + auth: { + required: Boolean(status.token), + scheme: 'Authorization: Bearer ' + }, + config: { + dbConfigReady: isDbConfigReady + } + } + + if (!verbose) { + return basePayload + } + + return { + ...basePayload, + usage: { + baseUrl: getBaseUrl(status), + health: '/v1/health', + status: '/v1/status', + auth: status.token ? 'Authorization: Bearer ' : 'No auth token required' + }, + app: { + version: getAppVersion(), + electronVersion: process.versions.electron, + nodeVersion: process.versions.node, + platform: process.platform + }, + debug: { + checks: { + apiEnabled: isApiEnabled, + apiRunning: isApiRunning, + dbConfigReady: isDbConfigReady, + authRequired: Boolean(status.token) + }, + tokenPreview: status.token ? `${status.token.slice(0, 3)}***${status.token.slice(-3)}` : '', + startedAt: status.startedAt ? new Date(status.startedAt).toISOString() : '', + lastError: status.startError + } + } +} + +export async function querySessions(input: QuerySessionsInput) { + const q = (input.q || '').trim().toLowerCase() + const typeFilter = parseSessionTypeFilter(input.type || null) + const unreadOnly = Boolean(input.unreadOnly) + const sort = (input.sort || 'sortTimestamp_desc').trim() + const offset = parseIntInRange(input.offset, 0, 0, 100000) + const limit = parseIntInRange(input.limit, 100, 1, 500) + + const sessionsResult = await chatService.getSessions() + if (!sessionsResult.success) { + throw new ApiQueryError( + 503, + 'DB_NOT_CONNECTED', + sessionsResult.error || 'Failed to read sessions', + 'Please complete DB decrypt/setup in Settings and ensure data is available.' + ) + } + + let sessions = (sessionsResult.sessions || []).map((item) => { + const sessionType = detectSessionType(item.username || '') + return { + username: item.username, + displayName: item.displayName || item.username, + avatarUrl: item.avatarUrl, + summary: item.summary, + unreadCount: item.unreadCount || 0, + sortTimestamp: item.sortTimestamp || 0, + lastTimestamp: item.lastTimestamp || 0, + lastMsgType: item.lastMsgType || 0, + sessionType + } + }) + + if (typeFilter) { + sessions = sessions.filter((item) => typeFilter.has(item.sessionType)) + } + + if (unreadOnly) { + sessions = sessions.filter((item) => Number(item.unreadCount || 0) > 0) + } + + if (q) { + sessions = sessions.filter((item) => { + const username = String(item.username || '').toLowerCase() + const displayName = String(item.displayName || '').toLowerCase() + const summary = String(item.summary || '').toLowerCase() + return username.includes(q) || displayName.includes(q) || summary.includes(q) + }) + } + + if (sort === 'name_asc') { + sessions.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''), 'zh-CN')) + } else if (sort === 'name_desc') { + sessions.sort((a, b) => String(b.displayName || '').localeCompare(String(a.displayName || ''), 'zh-CN')) + } else if (sort === 'lastTimestamp_asc') { + sessions.sort((a, b) => Number(a.lastTimestamp || 0) - Number(b.lastTimestamp || 0)) + } else if (sort === 'lastTimestamp_desc') { + sessions.sort((a, b) => Number(b.lastTimestamp || 0) - Number(a.lastTimestamp || 0)) + } else if (sort === 'unreadCount_desc') { + sessions.sort((a, b) => Number(b.unreadCount || 0) - Number(a.unreadCount || 0)) + } else { + sessions.sort((a, b) => Number(b.sortTimestamp || 0) - Number(a.sortTimestamp || 0)) + } + + const total = sessions.length + const paged = sessions.slice(offset, offset + limit) + const hasMore = offset + paged.length < total + + return { + total, + offset, + limit, + hasMore, + sort, + filters: { + q, + type: typeFilter ? Array.from(typeFilter) : null, + unreadOnly + }, + sessions: paged + } +} + +export async function queryMessages(input: QueryMessagesInput) { + const sessionId = (input.sessionId || '').trim() + if (!sessionId) { + throw new ApiQueryError( + 400, + 'BAD_REQUEST', + 'Missing required parameter: sessionId', + 'Use query parameter: sessionId=' + ) + } + + const offset = parseIntInRange(input.offset, 0, 0, 100000) + const limit = parseIntInRange(input.limit, 50, 1, 200) + const sort = (input.sort || 'createTime_desc').trim() + const keyword = (input.keyword || '').trim().toLowerCase() + const msgTypeFilter = input.msgType || null + const messageKindFilter = input.messageKind?.length ? new Set(input.messageKind.map((x) => x.toLowerCase())) : null + const appMsgTypeFilter = input.appMsgType?.length ? new Set(input.appMsgType.map((x) => x.toLowerCase())) : null + const startTimeMs = parseTimestampMs(input.startTime) + const endTimeMs = parseTimestampMs(input.endTime) + const includeRaw = Boolean(input.includeRaw) + const resolveMediaPath = parseBoolean(input.resolveMediaPath === undefined ? undefined : String(input.resolveMediaPath), true) + const resolveVoicePath = parseBoolean(input.resolveVoicePath === undefined ? undefined : String(input.resolveVoicePath), false) + const adaptive = parseBoolean(input.adaptive === undefined ? undefined : String(input.adaptive), true) + const maxScan = parseIntInRange(input.maxScan, 5000, 100, 20000) + const fields = parseFields(input.fields?.join(',') || null) + if (includeRaw) fields.add('raw') + + const includeField = (name: string): boolean => fields.has('all') || fields.has(name) + const needKindForFilter = Boolean(messageKindFilter || appMsgTypeFilter) + const needKindForOutput = [includeField('type'), includeField('metadata'), includeField('media')].some(Boolean) + const shouldResolveMediaPath = includeField('media') && resolveMediaPath + const shouldResolveVoicePath = includeField('media') && resolveVoicePath + const includeChatRecordItems = includeField('chatrecord') + + let myWxid = '' + let dbPath = '' + let cachePath = '' + if (shouldResolveVoicePath || shouldResolveMediaPath || includeField('file')) { + const runtimeConfig = new ConfigService() + myWxid = String(runtimeConfig.get('myWxid') || '') + dbPath = String(runtimeConfig.get('dbPath') || '') + cachePath = String(runtimeConfig.get('cachePath') || '') + runtimeConfig.close() + } + + const fetchBatchSize = 200 + const targetCount = offset + limit + let scanOffset = 0 + let scanned = 0 + let reachedEnd = false + const matched: any[] = [] + + while (scanned < maxScan && matched.length < targetCount) { + const part = await chatService.getMessages(sessionId, scanOffset, fetchBatchSize) + if (!part.success) { + throw new ApiQueryError( + 503, + 'DB_NOT_CONNECTED', + part.error || 'Failed to read messages', + 'Please complete DB decrypt/setup in Settings and ensure sessionId is correct.' + ) + } + + const chunk = part.messages || [] + if (chunk.length === 0) { + reachedEnd = true + break + } + + scanned += chunk.length + scanOffset += chunk.length + + for (const msg of chunk) { + if (msgTypeFilter && !msgTypeFilter.includes(Number(msg.localType || 0))) continue + + if (needKindForFilter) { + const kindInfo = detectMessageKind(msg as Record) + if (messageKindFilter && !messageKindFilter.has(kindInfo.messageKind)) continue + if (appMsgTypeFilter) { + const appMsgType = (kindInfo.appMsgType || '').toLowerCase() + if (!appMsgType || !appMsgTypeFilter.has(appMsgType)) continue + } + } + + const tMs = normalizeTimestampMs(Number(msg.createTime || 0)) + if (startTimeMs && tMs < startTimeMs) continue + if (endTimeMs && tMs > endTimeMs) continue + + if (keyword) { + const parsed = String(msg.parsedContent || '').toLowerCase() + const raw = String(msg.rawContent || '').toLowerCase() + if (!parsed.includes(keyword) && !raw.includes(keyword)) continue + } + + matched.push(msg) + } + + if (!part.hasMore) { + reachedEnd = true + break + } + } + + if (sort === 'createTime_asc') { + matched.sort((a, b) => Number(a.createTime || 0) - Number(b.createTime || 0)) + } else { + matched.sort((a, b) => Number(b.createTime || 0) - Number(a.createTime || 0)) + } + + const page = matched.slice(offset, offset + limit) + const hasMore = reachedEnd ? matched.length > offset + page.length : true + + const enrichOne = async (m: any): Promise> => { + const base = m as Record + const kind = needKindForOutput ? detectMessageKind(base) : { messageKind: 'unknown', typeLabel: '未知类型', appMsgType: undefined } + const createTimeMs = normalizeTimestampMs(Number(base.createTime || 0)) + const senderUsername = base.senderUsername || null + + const metadata = { + localType: Number(base.localType || 0), + messageKind: kind.messageKind, + typeLabel: kind.typeLabel, + appMsgType: kind.appMsgType || null, + direction: Number(base.isSend) === 1 ? 'out' : 'in', + isSystem: Number(base.localType || 0) === 10000 || kind.messageKind === 'app_pat', + isMedia: ['image', 'voice', 'video', 'emoji'].includes(kind.messageKind), + hasRawContent: Boolean(base.rawContent), + hasParsedContent: Boolean(base.parsedContent), + hasQuote: Boolean(base.quotedContent || base.quotedImageMd5 || base.quotedEmojiMd5), + hasFile: Boolean(base.fileName || base.fileMd5), + hasTransfer: Boolean(base.transferPayerUsername || base.transferReceiverUsername), + hasChatRecord: Array.isArray(base.chatRecordList) && base.chatRecordList.length > 0, + isLivePhoto: Boolean(base.isLivePhoto) + } + + const media = { + imageMd5: base.imageMd5 || null, + imageDatName: base.imageDatName || null, + imageCachePath: null as string | null, + emojiMd5: base.emojiMd5 || null, + emojiCdnUrl: base.emojiCdnUrl || null, + emojiCachePath: null as string | null, + videoMd5: base.videoMd5 || null, + videoDuration: base.videoDuration || null, + videoCachePath: null as string | null, + voiceDuration: base.voiceDuration || null, + voiceCachePath: null as string | null + } + + if (shouldResolveMediaPath && (kind.messageKind === 'emoji' || kind.messageKind.startsWith('app_')) && (base.emojiMd5 || base.emojiCdnUrl)) { + try { + const emojiResult = await chatService.downloadEmoji( + String(base.emojiCdnUrl || ''), + base.emojiMd5, + base.productId, + Number(base.createTime || 0), + base.emojiEncryptUrl, + base.emojiAesKey + ) + if (emojiResult.success && emojiResult.cachePath) { + media.emojiCachePath = emojiResult.cachePath + } + } catch { + // ignore + } + } + + if (shouldResolveMediaPath && kind.messageKind === 'image' && (base.imageMd5 || base.imageDatName)) { + try { + const resolved = await imageDecryptService.resolveCachedImage({ + sessionId, + imageMd5: base.imageMd5, + imageDatName: base.imageDatName + }) + + if (resolved.success && resolved.localPath) { + media.imageCachePath = fileUrlToPathMaybe(resolved.localPath) + } else { + const decrypted = await imageDecryptService.decryptImage({ + sessionId, + imageMd5: base.imageMd5, + imageDatName: base.imageDatName, + force: false + }) + if (decrypted.success && decrypted.localPath) { + media.imageCachePath = fileUrlToPathMaybe(decrypted.localPath) + } + } + } catch { + // ignore + } + } + + if (shouldResolveMediaPath && kind.messageKind === 'video' && base.videoMd5) { + try { + const videoInfo = videoService.getVideoInfo(String(base.videoMd5)) + if (videoInfo.exists && videoInfo.videoUrl) { + media.videoCachePath = fileUrlToPathMaybe(videoInfo.videoUrl) + } + } catch { + // ignore + } + } + + if (shouldResolveVoicePath && kind.messageKind === 'voice') { + try { + const voiceResult = await chatService.getVoiceData( + sessionId, + String(base.localId || ''), + Number(base.createTime || 0) + ) + if (voiceResult.success && voiceResult.data) { + const baseCacheDir = cachePath || join(process.cwd(), 'cache') + const voiceDir = join(baseCacheDir, 'HttpApiVoices', sanitizePathPart(sessionId)) + if (!existsSync(voiceDir)) { + mkdirSync(voiceDir, { recursive: true }) + } + const fileName = `${Number(base.createTime || 0)}_${Number(base.localId || 0)}.wav` + const absPath = join(voiceDir, fileName) + await writeFile(absPath, Buffer.from(voiceResult.data, 'base64')) + media.voiceCachePath = absPath + } + } catch { + // ignore + } + } + + const quote = metadata.hasQuote + ? { + content: base.quotedContent || null, + sender: base.quotedSender || null, + imageMd5: base.quotedImageMd5 || null, + emojiMd5: base.quotedEmojiMd5 || null, + emojiCdnUrl: base.quotedEmojiCdnUrl || null + } + : null + + const file = metadata.hasFile + ? { + name: base.fileName || null, + size: base.fileSize || null, + ext: base.fileExt || null, + md5: base.fileMd5 || null, + absolutePath: null as string | null, + exists: false + } + : null + + if (shouldResolveMediaPath && file?.name && dbPath && myWxid) { + try { + const msgDate = createTimeMs ? new Date(createTimeMs) : new Date() + const year = msgDate.getFullYear() + const month = String(msgDate.getMonth() + 1).padStart(2, '0') + const dateFolder = `${year}-${month}` + const abs = join(dbPath, myWxid, 'msg', 'file', dateFolder, String(file.name)) + file.absolutePath = abs + file.exists = existsSync(abs) + } catch { + // ignore + } + } + + const transfer = metadata.hasTransfer + ? { + payerUsername: base.transferPayerUsername || null, + receiverUsername: base.transferReceiverUsername || null + } + : null + + const chatRecord = metadata.hasChatRecord + ? { + count: Array.isArray(base.chatRecordList) ? base.chatRecordList.length : 0, + items: includeChatRecordItems ? (base.chatRecordList || []) : undefined + } + : null + + const out: Record = {} + + if (includeField('base')) { + out.localId = base.localId || 0 + out.serverId = base.serverId || 0 + out.localType = Number(base.localType || 0) + out.createTime = Number(base.createTime || 0) + out.sortSeq = Number(base.sortSeq || 0) + out.isSend = base.isSend ?? null + out.senderUsername = senderUsername + out.parsedContent = base.parsedContent || '' + } + + if (includeField('raw')) out.rawContent = base.rawContent || null + if (includeField('type')) { + out.messageKind = kind.messageKind + out.typeLabel = kind.typeLabel + out.appMsgType = kind.appMsgType || null + out.direction = metadata.direction + } + if (includeField('time')) { + out.createTimeMs = createTimeMs + out.createTimeIso = createTimeMs ? new Date(createTimeMs).toISOString() : null + } + if (includeField('sender')) { + out.sender = { + username: senderUsername, + isSelf: Number(base.isSend) === 1 + } + } + if (includeField('metadata')) out.metadata = metadata + if (includeField('media')) out.media = media + if (includeField('quote')) out.quote = quote + if (includeField('file')) out.file = file + if (includeField('transfer')) out.transfer = transfer + if (includeField('chatrecord')) out.chatRecord = chatRecord + + return out + } + + const enrichResults = await Promise.allSettled(page.map((m) => enrichOne(m))) + const enrichedMessages = enrichResults + .filter((r): r is PromiseFulfilledResult> => r.status === 'fulfilled') + .map((r) => r.value) + + const normalizedMessages = adaptive + ? enrichedMessages.map((m) => pruneEmpty(m)).filter(Boolean) + : enrichedMessages + + const finalMessages = includeRaw + ? normalizedMessages + : normalizedMessages.map((m) => { + const { rawContent, ...rest } = m as Record + return rest + }) + + const responsePayload: Record = { + sessionId, + total: reachedEnd ? matched.length : null, + offset, + limit, + hasMore, + scanned, + maxScan, + sort, + filters: { + keyword, + msgType: msgTypeFilter, + messageKind: messageKindFilter ? Array.from(messageKindFilter) : null, + appMsgType: appMsgTypeFilter ? Array.from(appMsgTypeFilter) : null, + startTime: startTimeMs, + endTime: endTimeMs, + includeRaw, + resolveMediaPath, + resolveVoicePath, + adaptive, + fields: Array.from(fields) + }, + messages: finalMessages + } + + if (includeField('schema')) { + responsePayload.messageTypeSchema = { + messageKind: { + text: '文本', + image: '图片', + voice: '语音', + video: '视频', + emoji: '表情', + location: '位置', + contact_card: '名片', + system: '系统消息', + quote: '引用消息', + voip: '音视频通话', + app_link: '链接', + app_file: '文件', + app_chat_record: '聊天记录', + app_mini_program: '小程序', + app_transfer: '转账', + app_red_packet: '红包', + app_announcement: '群公告', + app_pat: '拍一拍', + app_gift: '微信礼物', + app_music: '音乐分享', + app: '应用消息', + unknown: '未知类型' + }, + direction: { + out: '我发送', + in: '我接收' + } + } + } + + return responsePayload +} + +export async function queryContacts(input: QueryContactsInput) { + const q = (input.q || '').trim().toLowerCase() + const typeFilter = parseTypeFilter(input.type || null) + const includeAvatar = input.includeAvatar !== false + const sort = (input.sort || 'lastContactTime_desc').trim() + const offset = parseIntInRange(input.offset, 0, 0, 100000) + const limit = parseIntInRange(input.limit, 100, 1, 500) + + const contactsResult = await chatService.getContacts() + if (!contactsResult.success) { + throw new ApiQueryError( + 503, + 'DB_NOT_CONNECTED', + contactsResult.error || 'Failed to read contacts', + 'Please complete DB decrypt/setup in Settings and ensure data is available.' + ) + } + + let contacts = (contactsResult.contacts || []) as Array> + + if (typeFilter) { + contacts = contacts.filter((item) => typeFilter.has((item.type || 'other') as ContactType)) + } + + if (q) { + contacts = contacts.filter((item) => { + const username = String(item.username || '').toLowerCase() + const displayName = String(item.displayName || '').toLowerCase() + const remark = String(item.remark || '').toLowerCase() + const nickname = String(item.nickname || '').toLowerCase() + return ( + username.includes(q) || + displayName.includes(q) || + remark.includes(q) || + nickname.includes(q) + ) + }) + } + + if (sort === 'name_asc') { + contacts.sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''), 'zh-CN')) + } else if (sort === 'name_desc') { + contacts.sort((a, b) => String(b.displayName || '').localeCompare(String(a.displayName || ''), 'zh-CN')) + } else if (sort === 'lastContactTime_asc') { + contacts.sort((a, b) => Number((a as any).lastContactTime || 0) - Number((b as any).lastContactTime || 0)) + } else { + contacts.sort((a, b) => Number((b as any).lastContactTime || 0) - Number((a as any).lastContactTime || 0)) + } + + const total = contacts.length + const paged = contacts.slice(offset, offset + limit) + const hasMore = offset + paged.length < total + + const finalContacts = paged.map((item) => { + if (includeAvatar) return item + const { avatarUrl, ...rest } = item + return rest + }) + + return { + total, + offset, + limit, + hasMore, + sort, + filters: { + q, + type: typeFilter ? Array.from(typeFilter) : null, + includeAvatar + }, + contacts: finalContacts + } +} diff --git a/electron/services/imageDecryptService.ts b/electron/services/imageDecryptService.ts index b1f8864..f4a666e 100644 --- a/electron/services/imageDecryptService.ts +++ b/electron/services/imageDecryptService.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow } from 'electron' +import { BrowserWindow } from 'electron' import { basename, dirname, extname, join } from 'path' import { pathToFileURL } from 'url' import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs' @@ -9,6 +9,7 @@ import { Worker } from 'worker_threads' import { execFile } from 'child_process' import { promisify } from 'util' import { ConfigService } from './config' +import { getDocumentsPath, getExePath } from './runtimePaths' const execFileAsync = promisify(execFile) @@ -1506,19 +1507,19 @@ export class ImageDecryptService { private getDefaultCachePath(): string { // 开发环境使用文档目录 if (process.env.VITE_DEV_SERVER_URL) { - const documentsPath = app.getPath('documents') + const documentsPath = getDocumentsPath() return join(documentsPath, 'CipherTalkData') } // 生产环境 - const exePath = app.getPath('exe') + const exePath = getExePath() const installDir = require('path').dirname(exePath) // 检查是否安装在 C 盘 const isOnCDrive = /^[cC]:/i.test(installDir) || installDir.startsWith('\\\\') if (isOnCDrive) { - const documentsPath = app.getPath('documents') + const documentsPath = getDocumentsPath() return join(documentsPath, 'CipherTalkData') } @@ -1543,7 +1544,7 @@ export class ImageDecryptService { private getAllCacheRoots(): string[] { const roots: string[] = [] const configured = this.configService.get('cachePath') - const documentsPath = app.getPath('documents') + const documentsPath = getDocumentsPath() // 主要路径(当前使用的) const mainRoot = this.getCacheRoot() diff --git a/electron/services/mcp/server.ts b/electron/services/mcp/server.ts new file mode 100644 index 0000000..209ab3e --- /dev/null +++ b/electron/services/mcp/server.ts @@ -0,0 +1,163 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'zod' +import { ConfigService } from '../config' +import { + ApiQueryError, + queryContacts, + queryHealth, + queryMessages, + querySessions, + queryStatus +} from '../httpApiFacade' + +function formatToolResult(data: unknown, isError = false) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(data, null, 2) + } + ], + structuredContent: (data && typeof data === 'object' ? data : { value: data }) as Record, + isError + } +} + +function formatToolError(error: unknown) { + if (error instanceof ApiQueryError) { + return formatToolResult(error.toResponse(), true) + } + + return formatToolResult({ + code: 'INTERNAL_ERROR', + message: String(error) + }, true) +} + +function getHttpRuntimeStatus() { + const configService = new ConfigService() + const enabled = Boolean(configService.get('httpApiEnabled')) + const port = Number(configService.get('httpApiPort') || 5031) + const token = String(configService.get('httpApiToken') || '') + configService.close() + + return { + enabled, + running: enabled, + host: '127.0.0.1', + port, + startedAt: Date.now(), + token, + startError: '' + } +} + +function getMcpDefaults() { + const configService = new ConfigService() + const mcpEnabled = Boolean(configService.get('mcpEnabled')) + const mcpExposeMediaPaths = configService.get('mcpExposeMediaPaths') !== false + configService.close() + return { mcpEnabled, mcpExposeMediaPaths } +} + +export function createCipherTalkMcpServer() { + const server = new McpServer({ + name: 'ciphertalk-mcp', + version: '1.0.0' + }) + + server.registerTool('health_check', { + title: 'Health Check', + description: 'Return the embedded CipherTalk service health status.' + }, async () => { + return formatToolResult(queryHealth()) + }) + + server.registerTool('get_status', { + title: 'Get Status', + description: 'Return CipherTalk service and configuration status.', + inputSchema: { + verbose: z.boolean().optional().describe('Whether to include verbose debug and app details.') + } + }, async ({ verbose }) => { + try { + return formatToolResult(queryStatus(getHttpRuntimeStatus(), Boolean(verbose))) + } catch (error) { + return formatToolError(error) + } + }) + + server.registerTool('list_sessions', { + title: 'List Sessions', + description: 'List chat sessions with pagination, filtering, and sorting.', + inputSchema: { + q: z.string().optional().describe('Search keyword.'), + type: z.array(z.string()).optional().describe('Session types: friend, group, official, other.'), + unreadOnly: z.boolean().optional().describe('Only include sessions with unread messages.'), + sort: z.string().optional().describe('Sort mode.'), + offset: z.number().int().nonnegative().optional().describe('Pagination offset.'), + limit: z.number().int().positive().optional().describe('Pagination limit.') + } + }, async (args) => { + try { + return formatToolResult(await querySessions(args)) + } catch (error) { + return formatToolError(error) + } + }) + + server.registerTool('get_messages', { + title: 'Get Messages', + description: 'Query messages from a session with filtering, field selection, and optional media path resolution.', + inputSchema: { + sessionId: z.string().describe('Required session identifier / chat username.'), + offset: z.number().int().nonnegative().optional(), + limit: z.number().int().positive().optional(), + sort: z.string().optional(), + keyword: z.string().optional(), + msgType: z.array(z.number().int()).optional(), + messageKind: z.array(z.string()).optional(), + appMsgType: z.array(z.string()).optional(), + startTime: z.number().int().positive().optional().describe('Unix timestamp in seconds or milliseconds.'), + endTime: z.number().int().positive().optional().describe('Unix timestamp in seconds or milliseconds.'), + includeRaw: z.boolean().optional(), + resolveMediaPath: z.boolean().optional(), + resolveVoicePath: z.boolean().optional(), + adaptive: z.boolean().optional(), + maxScan: z.number().int().positive().optional(), + fields: z.array(z.string()).optional().describe('Requested field groups.') + } + }, async (args) => { + try { + const defaults = getMcpDefaults() + const resolveMediaPath = args.resolveMediaPath ?? defaults.mcpExposeMediaPaths + return formatToolResult(await queryMessages({ + ...args, + resolveMediaPath + })) + } catch (error) { + return formatToolError(error) + } + }) + + server.registerTool('list_contacts', { + title: 'List Contacts', + description: 'List contacts with pagination, filtering, and optional avatar fields.', + inputSchema: { + q: z.string().optional().describe('Search keyword.'), + type: z.array(z.string()).optional().describe('Contact types: friend, group, official, former_friend, other.'), + includeAvatar: z.boolean().optional(), + sort: z.string().optional(), + offset: z.number().int().nonnegative().optional(), + limit: z.number().int().positive().optional() + } + }, async (args) => { + try { + return formatToolResult(await queryContacts(args)) + } catch (error) { + return formatToolError(error) + } + }) + + return server +} diff --git a/electron/services/runtimePaths.ts b/electron/services/runtimePaths.ts new file mode 100644 index 0000000..e764691 --- /dev/null +++ b/electron/services/runtimePaths.ts @@ -0,0 +1,78 @@ +import os from 'os' +import path from 'path' + +function getElectronAppSafe(): any | null { + try { + // In Electron runtime this is the real module; in plain Node it may be a string path. + // We treat non-object values as unavailable. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const electronModule = require('electron') + if (electronModule && typeof electronModule === 'object' && electronModule.app) { + return electronModule.app + } + } catch { + // ignore + } + return null +} + +export function getUserDataPath(): string { + const app = getElectronAppSafe() + if (app?.getPath) { + return app.getPath('userData') + } + + const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') + return path.join(appData, 'ciphertalk') +} + +export function getDocumentsPath(): string { + const app = getElectronAppSafe() + if (app?.getPath) { + return app.getPath('documents') + } + + return path.join(os.homedir(), 'Documents') +} + +export function getExePath(): string { + const app = getElectronAppSafe() + if (app?.getPath) { + return app.getPath('exe') + } + + return process.execPath +} + +export function getAppPath(): string { + const app = getElectronAppSafe() + if (app?.getAppPath) { + return app.getAppPath() + } + + return process.cwd() +} + +export function isElectronPackaged(): boolean { + const app = getElectronAppSafe() + if (typeof app?.isPackaged === 'boolean') { + return app.isPackaged + } + + return !process.env.VITE_DEV_SERVER_URL +} + +export function getAppVersion(): string { + const app = getElectronAppSafe() + if (app?.getVersion) { + return app.getVersion() + } + + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const pkg = require('../../package.json') + return pkg.version || '0.0.0' + } catch { + return '0.0.0' + } +} diff --git a/electron/services/videoService.ts b/electron/services/videoService.ts index 2ae5f4a..e6ee8f6 100644 --- a/electron/services/videoService.ts +++ b/electron/services/videoService.ts @@ -3,10 +3,10 @@ import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, createWrite import { writeFile } from 'fs/promises' import { ConfigService } from './config' import Database from 'better-sqlite3' -import { app } from 'electron' import { Isaac64 } from './isaac64' import https from 'https' import http from 'http' +import { getDocumentsPath, getExePath } from './runtimePaths' export interface VideoInfo { videoUrl?: string // 视频文件路径(用�?readFile�? @@ -73,16 +73,16 @@ class VideoService { private getDefaultCachePath(): string { if (process.env.VITE_DEV_SERVER_URL) { - const documentsPath = app.getPath('documents') + const documentsPath = getDocumentsPath() return join(documentsPath, 'CipherTalkData') } - const exePath = app.getPath('exe') + const exePath = getExePath() const installDir = dirname(exePath) const isOnCDrive = /^[cC]:/i.test(installDir) || installDir.startsWith('\\') if (isOnCDrive) { - const documentsPath = app.getPath('documents') + const documentsPath = getDocumentsPath() return join(documentsPath, 'CipherTalkData') } diff --git a/package-lock.json b/package-lock.json index 4ce13de..586839b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "ciphertalk", - "version": "2.2.13", + "version": "2.2.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ciphertalk", - "version": "2.2.13", + "version": "2.2.14", "hasInstallScript": true, "license": "CC-BY-NC-SA-4.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@modelcontextprotocol/sdk": "^1.27.1", "@mui/material": "^7.3.9", "@types/dompurify": "^3.0.5", "@types/marked": "^5.0.2", @@ -46,6 +47,7 @@ "silk-wasm": "^3.7.1", "wechat-emojis": "^1.0.2", "xlsx": "^0.18.5", + "zod": "^4.1.12", "zustand": "^5.0.2" }, "devDependencies": { @@ -1354,6 +1356,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", @@ -1991,6 +2005,68 @@ "node": ">= 10.0.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.9", "resolved": "https://registry.npmmirror.com/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz", @@ -3299,6 +3375,44 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", @@ -4006,85 +4120,6 @@ "dev": true, "license": "ISC" }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/are-we-there-yet": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", @@ -4289,6 +4324,46 @@ "bluebird": "^3.5.5" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", @@ -4460,6 +4535,15 @@ "node": ">= 10.0.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmmirror.com/cacache/-/cacache-19.0.1.tgz", @@ -4706,6 +4790,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/callsites/-/callsites-3.1.0.tgz", @@ -4993,23 +5093,6 @@ "node": ">=0.10.0" } }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", @@ -5280,6 +5363,28 @@ "dev": true, "license": "ISC" }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5300,12 +5405,38 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -5354,26 +5485,10 @@ "node": ">=0.8" } }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5540,6 +5655,15 @@ "dev": true, "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5794,6 +5918,12 @@ "react": "^15.0.0 || >=16.0.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/ejs/-/ejs-3.1.10.tgz", @@ -5855,61 +5985,6 @@ "node": ">=14.0.0" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "25.1.8", - "resolved": "https://registry.npmmirror.com/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", - "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "25.1.8", - "archiver": "^5.3.1", - "builder-util": "25.1.7", - "fs-extra": "^10.1.0" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6113,10 +6188,20 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6260,6 +6345,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6272,6 +6363,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -6281,6 +6381,27 @@ "node": ">=6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", @@ -6297,6 +6418,101 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/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.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", @@ -6473,6 +6689,27 @@ "node": ">=10" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/find-root/-/find-root-1.1.0.tgz", @@ -6544,6 +6781,15 @@ "node": ">= 12.20" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/frac": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", @@ -6586,6 +6832,15 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", @@ -6970,6 +7225,15 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/hono": { + "version": "4.12.9", + "resolved": "https://registry.npmmirror.com/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -7023,6 +7287,26 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -7110,7 +7394,7 @@ "version": "0.6.3", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7223,12 +7507,20 @@ "version": "10.1.0", "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/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-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7315,6 +7607,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -7351,7 +7649,6 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jackspeak": { @@ -7394,6 +7691,15 @@ "integrity": "sha512-ZvQdS+FGifrFXZIXSgOyOgEz+1wdy1P4vSvwe37FVtku9ycSdHTZbHqF5i9tMN1JucoAmeiLBeI6/YaqcGD+KA==", "license": "MIT" }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7547,56 +7853,6 @@ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "license": "MIT" }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", @@ -7619,36 +7875,12 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "license": "MIT" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -7656,22 +7888,6 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7786,6 +8002,27 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/mime": { "version": "2.6.0", "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", @@ -8131,7 +8368,6 @@ "version": "1.0.0", "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8282,17 +8518,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmmirror.com/normalize-url/-/normalize-url-6.1.0.tgz", @@ -8332,6 +8557,18 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", @@ -8341,6 +8578,18 @@ "node": ">= 0.4" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/once/-/once-1.4.0.tgz", @@ -8545,6 +8794,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -8559,7 +8817,6 @@ "version": "3.1.1", "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8595,6 +8852,16 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/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/path-type": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", @@ -8645,6 +8912,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.0.tgz", @@ -8800,6 +9076,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/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.3", "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz", @@ -8820,6 +9109,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz", @@ -8833,6 +9137,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", @@ -8986,50 +9330,6 @@ "node": ">= 6" } }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", @@ -9233,6 +9533,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/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/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9257,7 +9573,6 @@ "version": "2.1.2", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, "license": "MIT" }, "node_modules/sanitize-filename": { @@ -9324,6 +9639,57 @@ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "license": "MIT" }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/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.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz", @@ -9351,6 +9717,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", @@ -9370,6 +9755,12 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", @@ -9419,7 +9810,6 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9432,7 +9822,6 @@ "version": "3.0.0", "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9520,6 +9909,78 @@ "win32" ] }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9729,6 +10190,15 @@ "node": ">= 6" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10026,6 +10496,15 @@ "tmp": "^0.2.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", @@ -10072,6 +10551,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", @@ -10146,6 +10664,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -10209,6 +10736,15 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/verror/-/verror-1.10.1.tgz", @@ -10376,7 +10912,6 @@ "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10560,43 +11095,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" } }, "node_modules/zrender": { diff --git a/package.json b/package.json index 21acff7..7c0eb6c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "prebuild": "node scripts/update-readme-version.js", "build:full": "node scripts/build-full.js", "build": "tsc && vite build && electron-builder && node scripts/add-size-to-yml.js", + "build:mcp": "tsc && vite build", "build:pro": "node scripts/build-full.js", + "mcp": "node scripts/mcp-runner.js", "preview": "vite preview", "electron:dev": "vite --mode electron", "electron:build": "npm run build", @@ -18,6 +20,7 @@ "tuisong": "node scripts/push-release.js" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/material": "^7.3.9", @@ -54,6 +57,7 @@ "silk-wasm": "^3.7.1", "wechat-emojis": "^1.0.2", "xlsx": "^0.18.5", + "zod": "^4.1.12", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/scripts/mcp-runner.js b/scripts/mcp-runner.js new file mode 100644 index 0000000..e47e6ac --- /dev/null +++ b/scripts/mcp-runner.js @@ -0,0 +1,41 @@ +const { spawn } = require('child_process') +const path = require('path') +const electronBinary = require('electron') + +const rootDir = path.resolve(__dirname, '..') +const entry = path.join(rootDir, 'dist-electron', 'mcp.js') + +const child = spawn(electronBinary, [entry], { + cwd: rootDir, + env: { + ...process.env, + ELECTRON_RUN_AS_NODE: '1' + }, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true +}) + +if (process.stdin) { + process.stdin.pipe(child.stdin) +} + +if (child.stdout) { + child.stdout.pipe(process.stdout) +} + +if (child.stderr) { + child.stderr.pipe(process.stderr) +} + +child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal) + return + } + process.exit(code ?? 0) +}) + +child.on('error', (error) => { + process.stderr.write(`[CipherTalk MCP Runner] failed: ${String(error)}\n`) + process.exit(1) +}) diff --git a/src/components/ai/AISummarySettings.tsx b/src/components/ai/AISummarySettings.tsx index 854143e..62e4a36 100644 --- a/src/components/ai/AISummarySettings.tsx +++ b/src/components/ai/AISummarySettings.tsx @@ -111,6 +111,10 @@ interface AISummarySettingsProps { setEnableThinking: (val: boolean) => void messageLimit: number setMessageLimit: (val: number) => void + mcpEnabled: boolean + setMcpEnabled: (val: boolean) => void + mcpExposeMediaPaths: boolean + setMcpExposeMediaPaths: (val: boolean) => void showMessage: (text: string, success: boolean) => void } @@ -133,6 +137,10 @@ function AISummarySettings({ setEnableThinking, messageLimit, setMessageLimit, + mcpEnabled, + setMcpEnabled, + mcpExposeMediaPaths, + setMcpExposeMediaPaths, showMessage }: AISummarySettingsProps) { const [showApiKey, setShowApiKey] = useState(false) @@ -750,6 +758,55 @@ function AISummarySettings({ )} +

MCP Server

+
+
+ +
+

为 Claude Desktop、Codex、Cherry Studio 等 MCP 宿主暴露 CipherTalk 数据读取能力。

+
+
+ +
+ +
+

控制 MCP `get_messages` 默认是否解析并返回图片、视频、文件等本地路径。

+
+
+ +
+ + +
+ 首批工具:`health_check`、`get_status`、`list_sessions`、`get_messages`、`list_contacts` +
+
+
+

💡 提示:API 密钥存储在本地,不会上传到任何服务器。摘要内容仅用于本地展示。

diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 6092081..9265a4f 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -140,6 +140,8 @@ function SettingsPage() { const [aiCustomSystemPrompt, setAiCustomSystemPromptState] = useState('') const [aiEnableThinking, setAiEnableThinkingState] = useState(true) const [aiMessageLimit, setAiMessageLimitState] = useState(3000) + const [mcpEnabled, setMcpEnabledState] = useState(false) + const [mcpExposeMediaPaths, setMcpExposeMediaPathsState] = useState(true) // 日志相关状态 const [logFiles, setLogFiles] = useState>([]) @@ -219,6 +221,8 @@ function SettingsPage() { const savedAiCustomSystemPrompt = await configService.getAiCustomSystemPrompt() const savedAiEnableThinking = await configService.getAiEnableThinking() const savedAiMessageLimit = await configService.getAiMessageLimit() + const savedMcpEnabled = await configService.getMcpEnabled() + const savedMcpExposeMediaPaths = await configService.getMcpExposeMediaPaths() setAiProviderState(savedAiProvider) setAiApiKeyState(savedAiApiKey) @@ -229,6 +233,8 @@ function SettingsPage() { setAiCustomSystemPromptState(savedAiCustomSystemPrompt) setAiEnableThinkingState(savedAiEnableThinking) setAiMessageLimitState(savedAiMessageLimit) + setMcpEnabledState(savedMcpEnabled) + setMcpExposeMediaPathsState(savedMcpExposeMediaPaths) // 加载关闭行为配置 const savedCloseToTray = await configService.getCloseToTray() @@ -262,6 +268,8 @@ function SettingsPage() { aiCustomSystemPrompt: savedAiCustomSystemPrompt, aiEnableThinking: savedAiEnableThinking, aiMessageLimit: savedAiMessageLimit, + mcpEnabled: savedMcpEnabled, + mcpExposeMediaPaths: savedMcpExposeMediaPaths, closeToTray: savedCloseToTray }) @@ -310,6 +318,8 @@ function SettingsPage() { aiCustomSystemPrompt, aiEnableThinking, aiMessageLimit, + mcpEnabled, + mcpExposeMediaPaths, closeToTray } @@ -323,6 +333,7 @@ function SettingsPage() { quoteStyle, exportDefaultDateRange, exportDefaultAvatars, aiProvider, aiApiKey, aiModel, aiDefaultTimeRange, aiSummaryDetail, aiSystemPromptPreset, aiCustomSystemPrompt, aiEnableThinking, aiMessageLimit, + mcpEnabled, mcpExposeMediaPaths, closeToTray, initialConfig ]) @@ -848,6 +859,8 @@ function SettingsPage() { await configService.setAiCustomSystemPrompt(aiCustomSystemPrompt) await configService.setAiEnableThinking(aiEnableThinking) await configService.setAiMessageLimit(aiMessageLimit) + await configService.setMcpEnabled(mcpEnabled) + await configService.setMcpExposeMediaPaths(mcpExposeMediaPaths) // 保存关闭行为配置 await configService.setCloseToTray(closeToTray) @@ -887,6 +900,8 @@ function SettingsPage() { aiCustomSystemPrompt, aiEnableThinking, aiMessageLimit, + mcpEnabled, + mcpExposeMediaPaths, closeToTray }) setHasUnsavedChanges(false) @@ -2787,6 +2802,10 @@ function SettingsPage() { setEnableThinking={setAiEnableThinkingState} messageLimit={aiMessageLimit} setMessageLimit={setAiMessageLimitState} + mcpEnabled={mcpEnabled} + setMcpEnabled={setMcpEnabledState} + mcpExposeMediaPaths={mcpExposeMediaPaths} + setMcpExposeMediaPaths={setMcpExposeMediaPathsState} showMessage={showMessage} /> )} diff --git a/src/services/config.ts b/src/services/config.ts index 7c4c9ed..bdc1b4d 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -28,6 +28,8 @@ export const CONFIG_KEYS = { HTTP_API_ENABLED: 'httpApiEnabled', HTTP_API_PORT: 'httpApiPort', HTTP_API_TOKEN: 'httpApiToken', + MCP_ENABLED: 'mcpEnabled', + MCP_EXPOSE_MEDIA_PATHS: 'mcpExposeMediaPaths', AUTH_ENABLED: 'authEnabled', AUTH_CREDENTIAL_ID: 'authCredentialId', AUTH_PASSWORD_HASH: 'authPasswordHash', @@ -509,6 +511,26 @@ export async function setAiMessageLimit(limit: number): Promise { await config.set('aiMessageLimit', limit) } +// --- MCP 配置 --- + +export async function getMcpEnabled(): Promise { + const value = await config.get(CONFIG_KEYS.MCP_ENABLED) + return value !== undefined ? (value as boolean) : false +} + +export async function setMcpEnabled(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.MCP_ENABLED, enabled) +} + +export async function getMcpExposeMediaPaths(): Promise { + const value = await config.get(CONFIG_KEYS.MCP_EXPOSE_MEDIA_PATHS) + return value !== undefined ? (value as boolean) : true +} + +export async function setMcpExposeMediaPaths(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.MCP_EXPOSE_MEDIA_PATHS, enabled) +} + // --- AI 配置预设 --- export interface AiConfigPreset { diff --git a/vite.config.ts b/vite.config.ts index f3977dc..375243d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -63,6 +63,15 @@ export default defineConfig({ rollupOptions: { external } } } + }, + { + entry: 'electron/mcp.ts', + vite: { + build: { + outDir: 'dist-electron', + rollupOptions: { external } + } + } } ]), renderer()