feat(mcp): add in-memory MCP server support and configuration management (#4359)
This commit is contained in:
parent
9c6de71fbb
commit
ea059d5517
2
.yarn/releases/yarn-4.6.0.cjs
vendored
2
.yarn/releases/yarn-4.6.0.cjs
vendored
File diff suppressed because one or more lines are too long
@ -70,6 +70,7 @@
|
|||||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
"diff": "^7.0.0",
|
||||||
"docx": "^9.0.2",
|
"docx": "^9.0.2",
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
@ -79,10 +80,14 @@
|
|||||||
"fast-xml-parser": "^5.0.9",
|
"fast-xml-parser": "^5.0.9",
|
||||||
"fetch-socks": "^1.3.2",
|
"fetch-socks": "^1.3.2",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"got-scraping": "^4.1.1",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"officeparser": "^4.1.1",
|
"officeparser": "^4.1.1",
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
|
"turndown": "^7.2.0",
|
||||||
|
"turndown-plugin-gfm": "^1.0.2",
|
||||||
"undici": "^7.4.0",
|
"undici": "^7.4.0",
|
||||||
"webdav": "^5.8.0",
|
"webdav": "^5.8.0",
|
||||||
"zipread": "^1.3.3"
|
"zipread": "^1.3.3"
|
||||||
@ -109,6 +114,7 @@
|
|||||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||||
"@tryfabric/martian": "^1.2.4",
|
"@tryfabric/martian": "^1.2.4",
|
||||||
"@types/adm-zip": "^0",
|
"@types/adm-zip": "^0",
|
||||||
|
"@types/diff": "^7",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
"@types/lodash": "^4.17.5",
|
"@types/lodash": "^4.17.5",
|
||||||
"@types/markdown-it": "^14",
|
"@types/markdown-it": "^14",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@ -43,8 +42,9 @@
|
|||||||
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
|
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
|
||||||
<p>
|
<p>
|
||||||
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
|
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
|
||||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
<a href="http://www.apache.org/licenses/LICENSE-2.0" class="text-blue-500 underline"
|
||||||
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
|
>http://www.apache.org/licenses/LICENSE-2.0</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
|
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
|
||||||
@ -57,28 +57,23 @@
|
|||||||
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
|
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
|
||||||
<ol class="list-decimal list-inside mb-4">
|
<ol class="list-decimal list-inside mb-4">
|
||||||
<li>
|
<li>
|
||||||
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without
|
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without modifying
|
||||||
modifying
|
|
||||||
the code.
|
the code.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Commercial License Required</strong>: A commercial license is required if any of the
|
<strong>Commercial License Required</strong>: A commercial license is required if any of the following
|
||||||
following
|
|
||||||
conditions are met:
|
conditions are met:
|
||||||
<ol class="list-decimal list-inside ml-4">
|
<ol class="list-decimal list-inside ml-4">
|
||||||
<li>
|
<li>
|
||||||
You modify, develop, or alter the software, including but not limited to changes to the
|
You modify, develop, or alter the software, including but not limited to changes to the application
|
||||||
application
|
|
||||||
name, logo, code, or functionality.
|
name, logo, code, or functionality.
|
||||||
</li>
|
</li>
|
||||||
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
|
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
|
||||||
<li>
|
<li>
|
||||||
You pre-install or integrate the software into hardware devices or products and bundle it
|
You pre-install or integrate the software into hardware devices or products and bundle it for sale.
|
||||||
for sale.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
You are engaging in large-scale procurement for government or educational institutions,
|
You are engaging in large-scale procurement for government or educational institutions, especially
|
||||||
especially
|
|
||||||
involving security, data privacy, or other sensitive requirements.
|
involving security, data privacy, or other sensitive requirements.
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@ -87,13 +82,11 @@
|
|||||||
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
|
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
|
||||||
<ol class="list-decimal list-inside mb-4">
|
<ol class="list-decimal list-inside mb-4">
|
||||||
<li>
|
<li>
|
||||||
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source
|
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source license as
|
||||||
license as
|
|
||||||
needed, making it stricter or more lenient.
|
needed, making it stricter or more lenient.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes,
|
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes, including but
|
||||||
including but
|
|
||||||
not limited to cloud business operations.
|
not limited to cloud business operations.
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@ -108,11 +101,11 @@
|
|||||||
<p>
|
<p>
|
||||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
|
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
|
||||||
License 2.0. Detailed information about the Apache License 2.0 can be found at
|
License 2.0. Detailed information about the Apache License 2.0 can be found at
|
||||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
<a href="http://www.apache.org/licenses/LICENSE-2.0" class="text-blue-500 underline"
|
||||||
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
|
>http://www.apache.org/licenses/LICENSE-2.0</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,6 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@ -18,7 +17,8 @@
|
|||||||
|
|
||||||
<!-- Loading状态 -->
|
<!-- Loading状态 -->
|
||||||
<div v-if="loading" class="text-center py-8">
|
<div v-if="loading" class="text-center py-8">
|
||||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
<div
|
||||||
|
class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
||||||
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -27,10 +27,14 @@
|
|||||||
|
|
||||||
<!-- Release 列表 -->
|
<!-- Release 列表 -->
|
||||||
<div v-else class="space-y-8">
|
<div v-else class="space-y-8">
|
||||||
<div v-for="release in releases" :key="release.id" class="relative pl-8"
|
<div
|
||||||
|
v-for="release in releases"
|
||||||
|
:key="release.id"
|
||||||
|
class="relative pl-8"
|
||||||
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||||
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
||||||
<div class="rounded-lg shadow-sm p-6 transition-shadow"
|
<div
|
||||||
|
class="rounded-lg shadow-sm p-6 transition-shadow"
|
||||||
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||||
<div class="flex items-start justify-between mb-4">
|
<div class="flex items-start justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
@ -41,12 +45,15 @@
|
|||||||
{{ formatDate(release.published_at) }}
|
{{ formatDate(release.published_at) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
<span
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||||
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||||
{{ release.tag_name }}
|
{{ release.tag_name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
|
<div
|
||||||
|
class="prose"
|
||||||
|
:class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
|
||||||
v-html="renderMarkdown(release.body)"></div>
|
v-html="renderMarkdown(release.body)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -198,5 +205,4 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
declare function decrypt(app: string, s: string): string;
|
declare function decrypt(app: string, s: string): string
|
||||||
|
|
||||||
interface Secret {
|
interface Secret {
|
||||||
app: string;
|
app: string
|
||||||
}
|
}
|
||||||
declare function createOAuthUrl(secret: Secret): string;
|
declare function createOAuthUrl(secret: Secret): string
|
||||||
|
|
||||||
export { type Secret, createOAuthUrl, decrypt };
|
export { type Secret, createOAuthUrl, decrypt }
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -26,7 +26,7 @@ import { TrayService } from './services/TrayService'
|
|||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
import { getResourcePath } from './utils'
|
import { getResourcePath } from './utils'
|
||||||
import { decrypt, encrypt } from './utils/aes'
|
import { decrypt, encrypt } from './utils/aes'
|
||||||
import { getFilesDir } from './utils/file'
|
import { getConfigDir, getFilesDir } from './utils/file'
|
||||||
import { compress, decompress } from './utils/zip'
|
import { compress, decompress } from './utils/zip'
|
||||||
|
|
||||||
const fileManager = new FileStorage()
|
const fileManager = new FileStorage()
|
||||||
@ -42,6 +42,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
isPackaged: app.isPackaged,
|
isPackaged: app.isPackaged,
|
||||||
appPath: app.getAppPath(),
|
appPath: app.getAppPath(),
|
||||||
filesPath: getFilesDir(),
|
filesPath: getFilesDir(),
|
||||||
|
configPath: getConfigDir(),
|
||||||
appDataPath: app.getPath('userData'),
|
appDataPath: app.getPath('userData'),
|
||||||
resourcesPath: getResourcePath(),
|
resourcesPath: getResourcePath(),
|
||||||
logsPath: log.transports.file.getFile().path
|
logsPath: log.transports.file.getFile().path
|
||||||
|
|||||||
374
src/main/mcpServers/brave-search.ts
Normal file
374
src/main/mcpServers/brave-search.ts
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
// Brave Search MCP Server
|
||||||
|
// port https://github.com/modelcontextprotocol/servers/blob/main/src/brave-search/index.ts
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
|
||||||
|
const WEB_SEARCH_TOOL: Tool = {
|
||||||
|
name: 'brave_web_search',
|
||||||
|
description:
|
||||||
|
'Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. ' +
|
||||||
|
'Use this for broad information gathering, recent events, or when you need diverse web sources. ' +
|
||||||
|
'Supports pagination, content filtering, and freshness controls. ' +
|
||||||
|
'Maximum 20 results per request, with offset for pagination. ',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Search query (max 400 chars, 50 words)'
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of results (1-20, default 10)',
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Pagination offset (max 9, default 0)',
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['query']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCAL_SEARCH_TOOL: Tool = {
|
||||||
|
name: 'brave_local_search',
|
||||||
|
description:
|
||||||
|
"Searches for local businesses and places using Brave's Local Search API. " +
|
||||||
|
'Best for queries related to physical locations, businesses, restaurants, services, etc. ' +
|
||||||
|
'Returns detailed information including:\n' +
|
||||||
|
'- Business names and addresses\n' +
|
||||||
|
'- Ratings and review counts\n' +
|
||||||
|
'- Phone numbers and opening hours\n' +
|
||||||
|
"Use this when the query implies 'near me' or mentions specific locations. " +
|
||||||
|
'Automatically falls back to web search if no local results are found.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: "Local search query (e.g. 'pizza near Central Park')"
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of results (1-20, default 5)',
|
||||||
|
default: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['query']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RATE_LIMIT = {
|
||||||
|
perSecond: 1,
|
||||||
|
perMonth: 15000
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestCount = {
|
||||||
|
second: 0,
|
||||||
|
month: 0,
|
||||||
|
lastReset: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRateLimit() {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - requestCount.lastReset > 1000) {
|
||||||
|
requestCount.second = 0
|
||||||
|
requestCount.lastReset = now
|
||||||
|
}
|
||||||
|
if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) {
|
||||||
|
throw new Error('Rate limit exceeded')
|
||||||
|
}
|
||||||
|
requestCount.second++
|
||||||
|
requestCount.month++
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BraveWeb {
|
||||||
|
web?: {
|
||||||
|
results?: Array<{
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
url: string
|
||||||
|
language?: string
|
||||||
|
published?: string
|
||||||
|
rank?: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
locations?: {
|
||||||
|
results?: Array<{
|
||||||
|
id: string // Required by API
|
||||||
|
title?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BraveLocation {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
address: {
|
||||||
|
streetAddress?: string
|
||||||
|
addressLocality?: string
|
||||||
|
addressRegion?: string
|
||||||
|
postalCode?: string
|
||||||
|
}
|
||||||
|
coordinates?: {
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
}
|
||||||
|
phone?: string
|
||||||
|
rating?: {
|
||||||
|
ratingValue?: number
|
||||||
|
ratingCount?: number
|
||||||
|
}
|
||||||
|
openingHours?: string[]
|
||||||
|
priceRange?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BravePoiResponse {
|
||||||
|
results: BraveLocation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BraveDescription {
|
||||||
|
descriptions: { [id: string]: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||||
|
return (
|
||||||
|
typeof args === 'object' &&
|
||||||
|
args !== null &&
|
||||||
|
'query' in args &&
|
||||||
|
typeof (args as { query: string }).query === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||||
|
return (
|
||||||
|
typeof args === 'object' &&
|
||||||
|
args !== null &&
|
||||||
|
'query' in args &&
|
||||||
|
typeof (args as { query: string }).query === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performWebSearch(apiKey: string, query: string, count: number = 10, offset: number = 0) {
|
||||||
|
checkRateLimit()
|
||||||
|
const url = new URL('https://api.search.brave.com/res/v1/web/search')
|
||||||
|
url.searchParams.set('q', query)
|
||||||
|
url.searchParams.set('count', Math.min(count, 20).toString()) // API limit
|
||||||
|
url.searchParams.set('offset', offset.toString())
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'X-Subscription-Token': apiKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as BraveWeb
|
||||||
|
|
||||||
|
// Extract just web results
|
||||||
|
const results = (data.web?.results || []).map((result) => ({
|
||||||
|
title: result.title || '',
|
||||||
|
description: result.description || '',
|
||||||
|
url: result.url || ''
|
||||||
|
}))
|
||||||
|
|
||||||
|
return results.map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`).join('\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performLocalSearch(apiKey: string, query: string, count: number = 5) {
|
||||||
|
checkRateLimit()
|
||||||
|
// Initial search to get location IDs
|
||||||
|
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search')
|
||||||
|
webUrl.searchParams.set('q', query)
|
||||||
|
webUrl.searchParams.set('search_lang', 'en')
|
||||||
|
webUrl.searchParams.set('result_filter', 'locations')
|
||||||
|
webUrl.searchParams.set('count', Math.min(count, 20).toString())
|
||||||
|
|
||||||
|
const webResponse = await fetch(webUrl, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'X-Subscription-Token': apiKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!webResponse.ok) {
|
||||||
|
throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const webData = (await webResponse.json()) as BraveWeb
|
||||||
|
const locationIds =
|
||||||
|
webData.locations?.results?.filter((r): r is { id: string; title?: string } => r.id != null).map((r) => r.id) || []
|
||||||
|
|
||||||
|
if (locationIds.length === 0) {
|
||||||
|
return performWebSearch(apiKey, query, count) // Fallback to web search
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get POI details and descriptions in parallel
|
||||||
|
const [poisData, descriptionsData] = await Promise.all([
|
||||||
|
getPoisData(apiKey, locationIds),
|
||||||
|
getDescriptionsData(apiKey, locationIds)
|
||||||
|
])
|
||||||
|
|
||||||
|
return formatLocalResults(poisData, descriptionsData)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPoisData(apiKey: string, ids: string[]): Promise<BravePoiResponse> {
|
||||||
|
checkRateLimit()
|
||||||
|
const url = new URL('https://api.search.brave.com/res/v1/local/pois')
|
||||||
|
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'X-Subscription-Token': apiKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const poisResponse = (await response.json()) as BravePoiResponse
|
||||||
|
return poisResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDescriptionsData(apiKey: string, ids: string[]): Promise<BraveDescription> {
|
||||||
|
checkRateLimit()
|
||||||
|
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions')
|
||||||
|
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'X-Subscription-Token': apiKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptionsData = (await response.json()) as BraveDescription
|
||||||
|
return descriptionsData
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {
|
||||||
|
return (
|
||||||
|
(poisData.results || [])
|
||||||
|
.map((poi) => {
|
||||||
|
const address =
|
||||||
|
[
|
||||||
|
poi.address?.streetAddress ?? '',
|
||||||
|
poi.address?.addressLocality ?? '',
|
||||||
|
poi.address?.addressRegion ?? '',
|
||||||
|
poi.address?.postalCode ?? ''
|
||||||
|
]
|
||||||
|
.filter((part) => part !== '')
|
||||||
|
.join(', ') || 'N/A'
|
||||||
|
|
||||||
|
return `Name: ${poi.name}
|
||||||
|
Address: ${address}
|
||||||
|
Phone: ${poi.phone || 'N/A'}
|
||||||
|
Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)
|
||||||
|
Price Range: ${poi.priceRange || 'N/A'}
|
||||||
|
Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}
|
||||||
|
Description: ${descData.descriptions[poi.id] || 'No description available'}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
.join('\n---\n') || 'No local results found'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BraveSearchServer {
|
||||||
|
public server: Server
|
||||||
|
private apiKey: string
|
||||||
|
|
||||||
|
constructor(apiKey: string) {
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('BRAVE_API_KEY is required for Brave Search MCP server')
|
||||||
|
}
|
||||||
|
this.apiKey = apiKey
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: 'brave-search-server',
|
||||||
|
version: '0.1.0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// Tool handlers
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL]
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
try {
|
||||||
|
const { name, arguments: args } = request.params
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No arguments provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case 'brave_web_search': {
|
||||||
|
if (!isBraveWebSearchArgs(args)) {
|
||||||
|
throw new Error('Invalid arguments for brave_web_search')
|
||||||
|
}
|
||||||
|
const { query, count = 10 } = args
|
||||||
|
const results = await performWebSearch(this.apiKey, query, count)
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: results }],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'brave_local_search': {
|
||||||
|
if (!isBraveLocalSearchArgs(args)) {
|
||||||
|
throw new Error('Invalid arguments for brave_local_search')
|
||||||
|
}
|
||||||
|
const { query, count = 5 } = args
|
||||||
|
const results = await performLocalSearch(this.apiKey, query, count)
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: results }],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BraveSearchServer
|
||||||
588
src/main/mcpServers/everything.ts
Normal file
588
src/main/mcpServers/everything.ts
Normal file
File diff suppressed because one or more lines are too long
36
src/main/mcpServers/factory.ts
Normal file
36
src/main/mcpServers/factory.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
|
import BraveSearchServer from './brave-search'
|
||||||
|
import EverythingServer from './everything'
|
||||||
|
import FetchServer from './fetch'
|
||||||
|
import FileSystemServer from './filesystem'
|
||||||
|
import MemoryServer from './memory'
|
||||||
|
import ThinkingServer from './sequentialthinking'
|
||||||
|
|
||||||
|
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server {
|
||||||
|
Logger.info(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`)
|
||||||
|
switch (name) {
|
||||||
|
case '@cherry/memory': {
|
||||||
|
const envPath = envs.MEMORY_FILE_PATH
|
||||||
|
return new MemoryServer(envPath).server
|
||||||
|
}
|
||||||
|
case '@cherry/sequentialthinking': {
|
||||||
|
return new ThinkingServer().server
|
||||||
|
}
|
||||||
|
case '@cherry/brave-search': {
|
||||||
|
return new BraveSearchServer(envs.BRAVE_API_KEY).server
|
||||||
|
}
|
||||||
|
case '@cherry/everything': {
|
||||||
|
return new EverythingServer().server
|
||||||
|
}
|
||||||
|
case '@cherry/fetch': {
|
||||||
|
return new FetchServer().server
|
||||||
|
}
|
||||||
|
case '@cherry/filesystem': {
|
||||||
|
return new FileSystemServer(args).server
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
236
src/main/mcpServers/fetch.ts
Normal file
236
src/main/mcpServers/fetch.ts
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
// port https://github.com/zcaceres/fetch-mcp/blob/main/src/index.ts
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { JSDOM } from 'jsdom'
|
||||||
|
import TurndownService from 'turndown'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const RequestPayloadSchema = z.object({
|
||||||
|
url: z.string().url(),
|
||||||
|
headers: z.record(z.string()).optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RequestPayload = z.infer<typeof RequestPayloadSchema>
|
||||||
|
|
||||||
|
export class Fetcher {
|
||||||
|
private static async _fetch({ url, headers }: RequestPayload): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
...headers
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error: ${response.status}`)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
throw new Error(`Failed to fetch ${url}: ${e.message}`)
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to fetch ${url}: Unknown error`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async html(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload)
|
||||||
|
const html = await response.text()
|
||||||
|
return { content: [{ type: 'text', text: html }], isError: false }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: (error as Error).message }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async json(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload)
|
||||||
|
const json = await response.json()
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(json) }],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: (error as Error).message }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async txt(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload)
|
||||||
|
const html = await response.text()
|
||||||
|
|
||||||
|
const dom = new JSDOM(html)
|
||||||
|
const document = dom.window.document
|
||||||
|
|
||||||
|
const scripts = document.getElementsByTagName('script')
|
||||||
|
const styles = document.getElementsByTagName('style')
|
||||||
|
Array.from(scripts).forEach((script: any) => script.remove())
|
||||||
|
Array.from(styles).forEach((style: any) => style.remove())
|
||||||
|
|
||||||
|
const text = document.body.textContent || ''
|
||||||
|
|
||||||
|
const normalizedText = text.replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: normalizedText }],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: (error as Error).message }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async markdown(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload)
|
||||||
|
const html = await response.text()
|
||||||
|
const turndownService = new TurndownService()
|
||||||
|
const markdown = turndownService.turndown(html)
|
||||||
|
return { content: [{ type: 'text', text: markdown }], isError: false }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: (error as Error).message }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'zcaceres/fetch',
|
||||||
|
version: '0.1.0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
resources: {},
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'fetch_html',
|
||||||
|
description: 'Fetch a website and return the content as HTML',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'URL of the website to fetch'
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Optional headers to include in the request'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fetch_markdown',
|
||||||
|
description: 'Fetch a website and return the content as Markdown',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'URL of the website to fetch'
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Optional headers to include in the request'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fetch_txt',
|
||||||
|
description: 'Fetch a website, return the content as plain text (no HTML)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'URL of the website to fetch'
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Optional headers to include in the request'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fetch_json',
|
||||||
|
description: 'Fetch a JSON file from a URL',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'URL of the JSON to fetch'
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Optional headers to include in the request'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { arguments: args } = request.params
|
||||||
|
|
||||||
|
const validatedArgs = RequestPayloadSchema.parse(args)
|
||||||
|
|
||||||
|
if (request.params.name === 'fetch_html') {
|
||||||
|
const fetchResult = await Fetcher.html(validatedArgs)
|
||||||
|
return fetchResult
|
||||||
|
}
|
||||||
|
if (request.params.name === 'fetch_json') {
|
||||||
|
const fetchResult = await Fetcher.json(validatedArgs)
|
||||||
|
return fetchResult
|
||||||
|
}
|
||||||
|
if (request.params.name === 'fetch_txt') {
|
||||||
|
const fetchResult = await Fetcher.txt(validatedArgs)
|
||||||
|
return fetchResult
|
||||||
|
}
|
||||||
|
if (request.params.name === 'fetch_markdown') {
|
||||||
|
const fetchResult = await Fetcher.markdown(validatedArgs)
|
||||||
|
return fetchResult
|
||||||
|
}
|
||||||
|
throw new Error('Tool not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
class FetchServer {
|
||||||
|
public server: Server
|
||||||
|
constructor() {
|
||||||
|
this.server = server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default FetchServer
|
||||||
655
src/main/mcpServers/filesystem.ts
Normal file
655
src/main/mcpServers/filesystem.ts
Normal file
@ -0,0 +1,655 @@
|
|||||||
|
// port https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { createTwoFilesPatch } from 'diff'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import { minimatch } from 'minimatch'
|
||||||
|
import os from 'os'
|
||||||
|
import path from 'path'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||||
|
|
||||||
|
// Normalize all paths consistently
|
||||||
|
function normalizePath(p: string): string {
|
||||||
|
return path.normalize(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandHome(filepath: string): string {
|
||||||
|
if (filepath.startsWith('~/') || filepath === '~') {
|
||||||
|
return path.join(os.homedir(), filepath.slice(1))
|
||||||
|
}
|
||||||
|
return filepath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security utilities
|
||||||
|
async function validatePath(allowedDirectories: string[], requestedPath: string): Promise<string> {
|
||||||
|
const expandedPath = expandHome(requestedPath)
|
||||||
|
const absolute = path.isAbsolute(expandedPath)
|
||||||
|
? path.resolve(expandedPath)
|
||||||
|
: path.resolve(process.cwd(), expandedPath)
|
||||||
|
|
||||||
|
const normalizedRequested = normalizePath(absolute)
|
||||||
|
|
||||||
|
// Check if path is within allowed directories
|
||||||
|
const isAllowed = allowedDirectories.some((dir) => normalizedRequested.startsWith(dir))
|
||||||
|
if (!isAllowed) {
|
||||||
|
throw new Error(
|
||||||
|
`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle symlinks by checking their real path
|
||||||
|
try {
|
||||||
|
const realPath = await fs.realpath(absolute)
|
||||||
|
const normalizedReal = normalizePath(realPath)
|
||||||
|
const isRealPathAllowed = allowedDirectories.some((dir) => normalizedReal.startsWith(dir))
|
||||||
|
if (!isRealPathAllowed) {
|
||||||
|
throw new Error('Access denied - symlink target outside allowed directories')
|
||||||
|
}
|
||||||
|
return realPath
|
||||||
|
} catch (error) {
|
||||||
|
// For new files that don't exist yet, verify parent directory
|
||||||
|
const parentDir = path.dirname(absolute)
|
||||||
|
try {
|
||||||
|
const realParentPath = await fs.realpath(parentDir)
|
||||||
|
const normalizedParent = normalizePath(realParentPath)
|
||||||
|
const isParentAllowed = allowedDirectories.some((dir) => normalizedParent.startsWith(dir))
|
||||||
|
if (!isParentAllowed) {
|
||||||
|
throw new Error('Access denied - parent directory outside allowed directories')
|
||||||
|
}
|
||||||
|
return absolute
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Parent directory does not exist: ${parentDir}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema definitions
|
||||||
|
const ReadFileArgsSchema = z.object({
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const ReadMultipleFilesArgsSchema = z.object({
|
||||||
|
paths: z.array(z.string())
|
||||||
|
})
|
||||||
|
|
||||||
|
const WriteFileArgsSchema = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
content: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const EditOperation = z.object({
|
||||||
|
oldText: z.string().describe('Text to search for - must match exactly'),
|
||||||
|
newText: z.string().describe('Text to replace with')
|
||||||
|
})
|
||||||
|
|
||||||
|
const EditFileArgsSchema = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
edits: z.array(EditOperation),
|
||||||
|
dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
|
||||||
|
})
|
||||||
|
|
||||||
|
const CreateDirectoryArgsSchema = z.object({
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const ListDirectoryArgsSchema = z.object({
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const DirectoryTreeArgsSchema = z.object({
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const MoveFileArgsSchema = z.object({
|
||||||
|
source: z.string(),
|
||||||
|
destination: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const SearchFilesArgsSchema = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
pattern: z.string(),
|
||||||
|
excludePatterns: z.array(z.string()).optional().default([])
|
||||||
|
})
|
||||||
|
|
||||||
|
const GetFileInfoArgsSchema = z.object({
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const ToolInputSchema = ToolSchema.shape.inputSchema
|
||||||
|
type ToolInput = z.infer<typeof ToolInputSchema>
|
||||||
|
|
||||||
|
interface FileInfo {
|
||||||
|
size: number
|
||||||
|
created: Date
|
||||||
|
modified: Date
|
||||||
|
accessed: Date
|
||||||
|
isDirectory: boolean
|
||||||
|
isFile: boolean
|
||||||
|
permissions: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool implementations
|
||||||
|
async function getFileStats(filePath: string): Promise<FileInfo> {
|
||||||
|
const stats = await fs.stat(filePath)
|
||||||
|
return {
|
||||||
|
size: stats.size,
|
||||||
|
created: stats.birthtime,
|
||||||
|
modified: stats.mtime,
|
||||||
|
accessed: stats.atime,
|
||||||
|
isDirectory: stats.isDirectory(),
|
||||||
|
isFile: stats.isFile(),
|
||||||
|
permissions: stats.mode.toString(8).slice(-3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchFiles(
|
||||||
|
allowedDirectories: string[],
|
||||||
|
rootPath: string,
|
||||||
|
pattern: string,
|
||||||
|
excludePatterns: string[] = []
|
||||||
|
): Promise<string[]> {
|
||||||
|
const results: string[] = []
|
||||||
|
|
||||||
|
async function search(currentPath: string) {
|
||||||
|
const entries = await fs.readdir(currentPath, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(currentPath, entry.name)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate each path before processing
|
||||||
|
await validatePath(allowedDirectories, fullPath)
|
||||||
|
|
||||||
|
// Check if path matches any exclude pattern
|
||||||
|
const relativePath = path.relative(rootPath, fullPath)
|
||||||
|
const shouldExclude = excludePatterns.some((pattern) => {
|
||||||
|
const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`
|
||||||
|
return minimatch(relativePath, globPattern, { dot: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (shouldExclude) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
|
||||||
|
results.push(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await search(fullPath)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Skip invalid paths during search
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await search(rootPath)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// file editing and diffing utilities
|
||||||
|
function normalizeLineEndings(text: string): string {
|
||||||
|
return text.replace(/\r\n/g, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
|
||||||
|
// Ensure consistent line endings for diff
|
||||||
|
const normalizedOriginal = normalizeLineEndings(originalContent)
|
||||||
|
const normalizedNew = normalizeLineEndings(newContent)
|
||||||
|
|
||||||
|
return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyFileEdits(
|
||||||
|
filePath: string,
|
||||||
|
edits: Array<{ oldText: string; newText: string }>,
|
||||||
|
dryRun = false
|
||||||
|
): Promise<string> {
|
||||||
|
// Read file content and normalize line endings
|
||||||
|
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'))
|
||||||
|
|
||||||
|
// Apply edits sequentially
|
||||||
|
let modifiedContent = content
|
||||||
|
for (const edit of edits) {
|
||||||
|
const normalizedOld = normalizeLineEndings(edit.oldText)
|
||||||
|
const normalizedNew = normalizeLineEndings(edit.newText)
|
||||||
|
|
||||||
|
// If exact match exists, use it
|
||||||
|
if (modifiedContent.includes(normalizedOld)) {
|
||||||
|
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try line-by-line matching with flexibility for whitespace
|
||||||
|
const oldLines = normalizedOld.split('\n')
|
||||||
|
const contentLines = modifiedContent.split('\n')
|
||||||
|
let matchFound = false
|
||||||
|
|
||||||
|
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
||||||
|
const potentialMatch = contentLines.slice(i, i + oldLines.length)
|
||||||
|
|
||||||
|
// Compare lines with normalized whitespace
|
||||||
|
const isMatch = oldLines.every((oldLine, j) => {
|
||||||
|
const contentLine = potentialMatch[j]
|
||||||
|
return oldLine.trim() === contentLine.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
// Preserve original indentation of first line
|
||||||
|
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''
|
||||||
|
const newLines = normalizedNew.split('\n').map((line, j) => {
|
||||||
|
if (j === 0) return originalIndent + line.trimStart()
|
||||||
|
// For subsequent lines, try to preserve relative indentation
|
||||||
|
const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''
|
||||||
|
const newIndent = line.match(/^\s*/)?.[0] || ''
|
||||||
|
if (oldIndent && newIndent) {
|
||||||
|
const relativeIndent = newIndent.length - oldIndent.length
|
||||||
|
return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart()
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
})
|
||||||
|
|
||||||
|
contentLines.splice(i, oldLines.length, ...newLines)
|
||||||
|
modifiedContent = contentLines.join('\n')
|
||||||
|
matchFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchFound) {
|
||||||
|
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unified diff
|
||||||
|
const diff = createUnifiedDiff(content, modifiedContent, filePath)
|
||||||
|
|
||||||
|
// Format diff with appropriate number of backticks
|
||||||
|
let numBackticks = 3
|
||||||
|
while (diff.includes('`'.repeat(numBackticks))) {
|
||||||
|
numBackticks++
|
||||||
|
}
|
||||||
|
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
await fs.writeFile(filePath, modifiedContent, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileSystemServer {
|
||||||
|
public server: Server
|
||||||
|
private allowedDirectories: string[]
|
||||||
|
constructor(allowedDirs: string[]) {
|
||||||
|
if (!Array.isArray(allowedDirs) || allowedDirs.length === 0) {
|
||||||
|
throw new Error('No allowed directories provided, please specify at least one directory in args')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allowedDirectories = allowedDirs.map((dir) => normalizePath(path.resolve(expandHome(dir))))
|
||||||
|
|
||||||
|
// Validate that all directories exist and are accessible
|
||||||
|
this.validateDirs().catch((error) => {
|
||||||
|
console.error('Error validating allowed directories:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: 'secure-filesystem-server',
|
||||||
|
version: '0.2.0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateDirs() {
|
||||||
|
// Validate that all directories exist and are accessible
|
||||||
|
await Promise.all(
|
||||||
|
this.allowedDirectories.map(async (dir) => {
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(expandHome(dir))
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
console.error(`Error: ${dir} is not a directory`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error accessing directory ${dir}:`, error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// Tool handlers
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'read_file',
|
||||||
|
description:
|
||||||
|
'Read the complete contents of a file from the file system. ' +
|
||||||
|
'Handles various text encodings and provides detailed error messages ' +
|
||||||
|
'if the file cannot be read. Use this tool when you need to examine ' +
|
||||||
|
'the contents of a single file. Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'read_multiple_files',
|
||||||
|
description:
|
||||||
|
'Read the contents of multiple files simultaneously. This is more ' +
|
||||||
|
'efficient than reading files one by one when you need to analyze ' +
|
||||||
|
"or compare multiple files. Each file's content is returned with its " +
|
||||||
|
"path as a reference. Failed reads for individual files won't stop " +
|
||||||
|
'the entire operation. Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'write_file',
|
||||||
|
description:
|
||||||
|
'Create a new file or completely overwrite an existing file with new content. ' +
|
||||||
|
'Use with caution as it will overwrite existing files without warning. ' +
|
||||||
|
'Handles text content with proper encoding. Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit_file',
|
||||||
|
description:
|
||||||
|
'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
|
||||||
|
'with new content. Returns a git-style diff showing the changes made. ' +
|
||||||
|
'Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_directory',
|
||||||
|
description:
|
||||||
|
'Create a new directory or ensure a directory exists. Can create multiple ' +
|
||||||
|
'nested directories in one operation. If the directory already exists, ' +
|
||||||
|
'this operation will succeed silently. Perfect for setting up directory ' +
|
||||||
|
'structures for projects or ensuring required paths exist. Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list_directory',
|
||||||
|
description:
|
||||||
|
'Get a detailed listing of all files and directories in a specified path. ' +
|
||||||
|
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
|
||||||
|
'prefixes. This tool is essential for understanding directory structure and ' +
|
||||||
|
'finding specific files within a directory. Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'directory_tree',
|
||||||
|
description:
|
||||||
|
'Get a recursive tree view of files and directories as a JSON structure. ' +
|
||||||
|
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
||||||
|
'Files have no children array, while directories always have a children array (which may be empty). ' +
|
||||||
|
'The output is formatted with 2-space indentation for readability. Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'move_file',
|
||||||
|
description:
|
||||||
|
'Move or rename files and directories. Can move files between directories ' +
|
||||||
|
'and rename them in a single operation. If the destination exists, the ' +
|
||||||
|
'operation will fail. Works across different directories and can be used ' +
|
||||||
|
'for simple renaming within the same directory. Both source and destination must be within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'search_files',
|
||||||
|
description:
|
||||||
|
'Recursively search for files and directories matching a pattern. ' +
|
||||||
|
'Searches through all subdirectories from the starting path. The search ' +
|
||||||
|
'is case-insensitive and matches partial names. Returns full paths to all ' +
|
||||||
|
"matching items. Great for finding files when you don't know their exact location. " +
|
||||||
|
'Only searches within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_file_info',
|
||||||
|
description:
|
||||||
|
'Retrieve detailed metadata about a file or directory. Returns comprehensive ' +
|
||||||
|
'information including size, creation time, last modified time, permissions, ' +
|
||||||
|
'and type. This tool is perfect for understanding file characteristics ' +
|
||||||
|
'without reading the actual content. Only works within allowed directories.',
|
||||||
|
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list_allowed_directories',
|
||||||
|
description:
|
||||||
|
'Returns the list of directories that this server is allowed to access. ' +
|
||||||
|
'Use this to understand which directories are available before trying to access files.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
try {
|
||||||
|
const { name, arguments: args } = request.params
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case 'read_file': {
|
||||||
|
const parsed = ReadFileArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for read_file: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||||
|
const content = await fs.readFile(validPath, 'utf-8')
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: content }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'read_multiple_files': {
|
||||||
|
const parsed = ReadMultipleFilesArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const results = await Promise.all(
|
||||||
|
parsed.data.paths.map(async (filePath: string) => {
|
||||||
|
try {
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, filePath)
|
||||||
|
const content = await fs.readFile(validPath, 'utf-8')
|
||||||
|
return `${filePath}:\n${content}\n`
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
return `${filePath}: Error - ${errorMessage}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: results.join('\n---\n') }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'write_file': {
|
||||||
|
const parsed = WriteFileArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for write_file: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||||
|
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Successfully wrote to ${parsed.data.path}` }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'edit_file': {
|
||||||
|
const parsed = EditFileArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||||
|
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun)
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: result }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'create_directory': {
|
||||||
|
const parsed = CreateDirectoryArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||||
|
await fs.mkdir(validPath, { recursive: true })
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Successfully created directory ${parsed.data.path}` }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list_directory': {
|
||||||
|
const parsed = ListDirectoryArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||||
|
const entries = await fs.readdir(validPath, { withFileTypes: true })
|
||||||
|
const formatted = entries
|
||||||
|
.map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
|
||||||
|
.join('\n')
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: formatted }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'directory_tree': {
|
||||||
|
const parsed = DirectoryTreeArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeEntry {
|
||||||
|
name: string
|
||||||
|
type: 'file' | 'directory'
|
||||||
|
children?: TreeEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildTree(allowedDirectories: string[], currentPath: string): Promise<TreeEntry[]> {
|
||||||
|
const validPath = await validatePath(allowedDirectories, currentPath)
|
||||||
|
const entries = await fs.readdir(validPath, { withFileTypes: true })
|
||||||
|
const result: TreeEntry[] = []
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryData: TreeEntry = {
|
||||||
|
name: entry.name,
|
||||||
|
type: entry.isDirectory() ? 'directory' : 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const subPath = path.join(currentPath, entry.name)
|
||||||
|
entryData.children = await buildTree(allowedDirectories, subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(entryData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = await buildTree(this.allowedDirectories, parsed.data.path)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(treeData, null, 2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'move_file': {
|
||||||
|
const parsed = MoveFileArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for move_file: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validSourcePath = await validatePath(this.allowedDirectories, parsed.data.source)
|
||||||
|
const validDestPath = await validatePath(this.allowedDirectories, parsed.data.destination)
|
||||||
|
await fs.rename(validSourcePath, validDestPath)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'search_files': {
|
||||||
|
const parsed = SearchFilesArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for search_files: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||||
|
const results = await searchFiles(
|
||||||
|
this.allowedDirectories,
|
||||||
|
validPath,
|
||||||
|
parsed.data.pattern,
|
||||||
|
parsed.data.excludePatterns
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: results.length > 0 ? results.join('\n') : 'No matches found' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_file_info': {
|
||||||
|
const parsed = GetFileInfoArgsSchema.safeParse(args)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`)
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||||
|
const info = await getFileStats(validPath)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: Object.entries(info)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list_allowed_directories': {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Allowed directories:\n${this.allowedDirectories.join('\n')}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${name}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileSystemServer
|
||||||
490
src/main/mcpServers/memory.ts
Normal file
490
src/main/mcpServers/memory.ts
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
// port https://github.com/modelcontextprotocol/servers/blob/main/src/memory/index.ts
|
||||||
|
import { getConfigDir } from '@main/utils/file'
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
// Define memory file path using environment variable with fallback
|
||||||
|
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')
|
||||||
|
|
||||||
|
// We are storing our memory using entities, relations, and observations in a graph structure
|
||||||
|
interface Entity {
|
||||||
|
name: string
|
||||||
|
entityType: string
|
||||||
|
observations: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Relation {
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
relationType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KnowledgeGraph {
|
||||||
|
entities: Entity[]
|
||||||
|
relations: Relation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
|
||||||
|
class KnowledgeGraphManager {
|
||||||
|
private memoryPath: string
|
||||||
|
|
||||||
|
constructor(memoryPath: string) {
|
||||||
|
this.memoryPath = memoryPath
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadGraph(): Promise<KnowledgeGraph> {
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(this.memoryPath, 'utf-8')
|
||||||
|
const lines = data.split('\n').filter((line) => line.trim() !== '')
|
||||||
|
return lines.reduce(
|
||||||
|
(graph: KnowledgeGraph, line) => {
|
||||||
|
const item = JSON.parse(line)
|
||||||
|
if (item.type === 'entity') graph.entities.push(item as Entity)
|
||||||
|
if (item.type === 'relation') graph.relations.push(item as Relation)
|
||||||
|
return graph
|
||||||
|
},
|
||||||
|
{ entities: [], relations: [] }
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
||||||
|
return { entities: [], relations: [] }
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveGraph(graph: KnowledgeGraph): Promise<void> {
|
||||||
|
const lines = [
|
||||||
|
...graph.entities.map((e) => JSON.stringify({ type: 'entity', ...e })),
|
||||||
|
...graph.relations.map((r) => JSON.stringify({ type: 'relation', ...r }))
|
||||||
|
]
|
||||||
|
await fs.writeFile(this.memoryPath, lines.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async createEntities(entities: Entity[]): Promise<Entity[]> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
const newEntities = entities.filter((e) => !graph.entities.some((existingEntity) => existingEntity.name === e.name))
|
||||||
|
graph.entities.push(...newEntities)
|
||||||
|
await this.saveGraph(graph)
|
||||||
|
return newEntities
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRelations(relations: Relation[]): Promise<Relation[]> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
const newRelations = relations.filter(
|
||||||
|
(r) =>
|
||||||
|
!graph.relations.some(
|
||||||
|
(existingRelation) =>
|
||||||
|
existingRelation.from === r.from &&
|
||||||
|
existingRelation.to === r.to &&
|
||||||
|
existingRelation.relationType === r.relationType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
graph.relations.push(...newRelations)
|
||||||
|
await this.saveGraph(graph)
|
||||||
|
return newRelations
|
||||||
|
}
|
||||||
|
|
||||||
|
async addObservations(
|
||||||
|
observations: { entityName: string; contents: string[] }[]
|
||||||
|
): Promise<{ entityName: string; addedObservations: string[] }[]> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
const results = observations.map((o) => {
|
||||||
|
const entity = graph.entities.find((e) => e.name === o.entityName)
|
||||||
|
if (!entity) {
|
||||||
|
throw new Error(`Entity with name ${o.entityName} not found`)
|
||||||
|
}
|
||||||
|
const newObservations = o.contents.filter((content) => !entity.observations.includes(content))
|
||||||
|
entity.observations.push(...newObservations)
|
||||||
|
return { entityName: o.entityName, addedObservations: newObservations }
|
||||||
|
})
|
||||||
|
await this.saveGraph(graph)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEntities(entityNames: string[]): Promise<void> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
graph.entities = graph.entities.filter((e) => !entityNames.includes(e.name))
|
||||||
|
graph.relations = graph.relations.filter((r) => !entityNames.includes(r.from) && !entityNames.includes(r.to))
|
||||||
|
await this.saveGraph(graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
deletions.forEach((d) => {
|
||||||
|
const entity = graph.entities.find((e) => e.name === d.entityName)
|
||||||
|
if (entity) {
|
||||||
|
entity.observations = entity.observations.filter((o) => !d.observations.includes(o))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await this.saveGraph(graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRelations(relations: Relation[]): Promise<void> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
graph.relations = graph.relations.filter(
|
||||||
|
(r) =>
|
||||||
|
!relations.some(
|
||||||
|
(delRelation) =>
|
||||||
|
r.from === delRelation.from && r.to === delRelation.to && r.relationType === delRelation.relationType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await this.saveGraph(graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
async readGraph(): Promise<KnowledgeGraph> {
|
||||||
|
return this.loadGraph()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Very basic search function
|
||||||
|
async searchNodes(query: string): Promise<KnowledgeGraph> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
|
||||||
|
// Filter entities
|
||||||
|
const filteredEntities = graph.entities.filter(
|
||||||
|
(e) =>
|
||||||
|
e.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
e.observations.some((o) => o.toLowerCase().includes(query.toLowerCase()))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a Set of filtered entity names for quick lookup
|
||||||
|
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
|
||||||
|
|
||||||
|
// Filter relations to only include those between filtered entities
|
||||||
|
const filteredRelations = graph.relations.filter(
|
||||||
|
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredGraph: KnowledgeGraph = {
|
||||||
|
entities: filteredEntities,
|
||||||
|
relations: filteredRelations
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
async openNodes(names: string[]): Promise<KnowledgeGraph> {
|
||||||
|
const graph = await this.loadGraph()
|
||||||
|
|
||||||
|
// Filter entities
|
||||||
|
const filteredEntities = graph.entities.filter((e) => names.includes(e.name))
|
||||||
|
|
||||||
|
// Create a Set of filtered entity names for quick lookup
|
||||||
|
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
|
||||||
|
|
||||||
|
// Filter relations to only include those between filtered entities
|
||||||
|
const filteredRelations = graph.relations.filter(
|
||||||
|
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredGraph: KnowledgeGraph = {
|
||||||
|
entities: filteredEntities,
|
||||||
|
relations: filteredRelations
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredGraph
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryServer {
|
||||||
|
public server: Server
|
||||||
|
private knowledgeGraphManager: KnowledgeGraphManager
|
||||||
|
|
||||||
|
constructor(envPath: string = '') {
|
||||||
|
const memoryPath = envPath
|
||||||
|
? path.isAbsolute(envPath)
|
||||||
|
? envPath
|
||||||
|
: path.join(path.dirname(fileURLToPath(import.meta.url)), envPath)
|
||||||
|
: defaultMemoryPath
|
||||||
|
this.knowledgeGraphManager = new KnowledgeGraphManager(memoryPath)
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: 'memory-server',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// The server instance and tools exposed to Claude
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'create_entities',
|
||||||
|
description: 'Create multiple new entities in the knowledge graph',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
entities: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string', description: 'The name of the entity' },
|
||||||
|
entityType: { type: 'string', description: 'The type of the entity' },
|
||||||
|
observations: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'An array of observation contents associated with the entity'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['name', 'entityType', 'observations']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['entities']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_relations',
|
||||||
|
description:
|
||||||
|
'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
relations: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
from: { type: 'string', description: 'The name of the entity where the relation starts' },
|
||||||
|
to: { type: 'string', description: 'The name of the entity where the relation ends' },
|
||||||
|
relationType: { type: 'string', description: 'The type of the relation' }
|
||||||
|
},
|
||||||
|
required: ['from', 'to', 'relationType']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['relations']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'add_observations',
|
||||||
|
description: 'Add new observations to existing entities in the knowledge graph',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
observations: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
entityName: { type: 'string', description: 'The name of the entity to add the observations to' },
|
||||||
|
contents: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'An array of observation contents to add'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['entityName', 'contents']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['observations']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete_entities',
|
||||||
|
description: 'Delete multiple entities and their associated relations from the knowledge graph',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
entityNames: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'An array of entity names to delete'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['entityNames']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete_observations',
|
||||||
|
description: 'Delete specific observations from entities in the knowledge graph',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
deletions: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
entityName: { type: 'string', description: 'The name of the entity containing the observations' },
|
||||||
|
observations: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'An array of observations to delete'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['entityName', 'observations']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['deletions']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete_relations',
|
||||||
|
description: 'Delete multiple relations from the knowledge graph',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
relations: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
from: { type: 'string', description: 'The name of the entity where the relation starts' },
|
||||||
|
to: { type: 'string', description: 'The name of the entity where the relation ends' },
|
||||||
|
relationType: { type: 'string', description: 'The type of the relation' }
|
||||||
|
},
|
||||||
|
required: ['from', 'to', 'relationType']
|
||||||
|
},
|
||||||
|
description: 'An array of relations to delete'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['relations']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'read_graph',
|
||||||
|
description: 'Read the entire knowledge graph',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'search_nodes',
|
||||||
|
description: 'Search for nodes in the knowledge graph based on a query',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The search query to match against entity names, types, and observation content'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['query']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'open_nodes',
|
||||||
|
description: 'Open specific nodes in the knowledge graph by their names',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
names: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'An array of entity names to retrieve'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['names']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error(`No arguments provided for tool: ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case 'create_entities':
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(
|
||||||
|
await this.knowledgeGraphManager.createEntities(args.entities as Entity[]),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
case 'create_relations':
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(
|
||||||
|
await this.knowledgeGraphManager.createRelations(args.relations as Relation[]),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
case 'add_observations':
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(
|
||||||
|
await this.knowledgeGraphManager.addObservations(
|
||||||
|
args.observations as { entityName: string; contents: string[] }[]
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
case 'delete_entities':
|
||||||
|
await this.knowledgeGraphManager.deleteEntities(args.entityNames as string[])
|
||||||
|
return { content: [{ type: 'text', text: 'Entities deleted successfully' }] }
|
||||||
|
case 'delete_observations':
|
||||||
|
await this.knowledgeGraphManager.deleteObservations(
|
||||||
|
args.deletions as { entityName: string; observations: string[] }[]
|
||||||
|
)
|
||||||
|
return { content: [{ type: 'text', text: 'Observations deleted successfully' }] }
|
||||||
|
case 'delete_relations':
|
||||||
|
await this.knowledgeGraphManager.deleteRelations(args.relations as Relation[])
|
||||||
|
return { content: [{ type: 'text', text: 'Relations deleted successfully' }] }
|
||||||
|
case 'read_graph':
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(await this.knowledgeGraphManager.readGraph(), null, 2) }]
|
||||||
|
}
|
||||||
|
case 'search_nodes':
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(await this.knowledgeGraphManager.searchNodes(args.query as string), null, 2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
case 'open_nodes':
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(await this.knowledgeGraphManager.openNodes(args.names as string[]), null, 2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${name}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MemoryServer
|
||||||
289
src/main/mcpServers/sequentialthinking.ts
Normal file
289
src/main/mcpServers/sequentialthinking.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
// Sequential Thinking MCP Server
|
||||||
|
// port https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
// Fixed chalk import for ESM
|
||||||
|
import chalk from 'chalk'
|
||||||
|
|
||||||
|
interface ThoughtData {
|
||||||
|
thought: string
|
||||||
|
thoughtNumber: number
|
||||||
|
totalThoughts: number
|
||||||
|
isRevision?: boolean
|
||||||
|
revisesThought?: number
|
||||||
|
branchFromThought?: number
|
||||||
|
branchId?: string
|
||||||
|
needsMoreThoughts?: boolean
|
||||||
|
nextThoughtNeeded: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class SequentialThinkingServer {
|
||||||
|
private thoughtHistory: ThoughtData[] = []
|
||||||
|
private branches: Record<string, ThoughtData[]> = {}
|
||||||
|
|
||||||
|
private validateThoughtData(input: unknown): ThoughtData {
|
||||||
|
const data = input as Record<string, unknown>
|
||||||
|
|
||||||
|
if (!data.thought || typeof data.thought !== 'string') {
|
||||||
|
throw new Error('Invalid thought: must be a string')
|
||||||
|
}
|
||||||
|
if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') {
|
||||||
|
throw new Error('Invalid thoughtNumber: must be a number')
|
||||||
|
}
|
||||||
|
if (!data.totalThoughts || typeof data.totalThoughts !== 'number') {
|
||||||
|
throw new Error('Invalid totalThoughts: must be a number')
|
||||||
|
}
|
||||||
|
if (typeof data.nextThoughtNeeded !== 'boolean') {
|
||||||
|
throw new Error('Invalid nextThoughtNeeded: must be a boolean')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
thought: data.thought,
|
||||||
|
thoughtNumber: data.thoughtNumber,
|
||||||
|
totalThoughts: data.totalThoughts,
|
||||||
|
nextThoughtNeeded: data.nextThoughtNeeded,
|
||||||
|
isRevision: data.isRevision as boolean | undefined,
|
||||||
|
revisesThought: data.revisesThought as number | undefined,
|
||||||
|
branchFromThought: data.branchFromThought as number | undefined,
|
||||||
|
branchId: data.branchId as string | undefined,
|
||||||
|
needsMoreThoughts: data.needsMoreThoughts as boolean | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatThought(thoughtData: ThoughtData): string {
|
||||||
|
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } =
|
||||||
|
thoughtData
|
||||||
|
|
||||||
|
let prefix = ''
|
||||||
|
let context = ''
|
||||||
|
|
||||||
|
if (isRevision) {
|
||||||
|
prefix = chalk.yellow('🔄 Revision')
|
||||||
|
context = ` (revising thought ${revisesThought})`
|
||||||
|
} else if (branchFromThought) {
|
||||||
|
prefix = chalk.green('🌿 Branch')
|
||||||
|
context = ` (from thought ${branchFromThought}, ID: ${branchId})`
|
||||||
|
} else {
|
||||||
|
prefix = chalk.blue('💭 Thought')
|
||||||
|
context = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`
|
||||||
|
const border = '─'.repeat(Math.max(header.length, thought.length) + 4)
|
||||||
|
|
||||||
|
return `
|
||||||
|
┌${border}┐
|
||||||
|
│ ${header} │
|
||||||
|
├${border}┤
|
||||||
|
│ ${thought.padEnd(border.length - 2)} │
|
||||||
|
└${border}┘`
|
||||||
|
}
|
||||||
|
|
||||||
|
public processThought(input: unknown): { content: Array<{ type: string; text: string }>; isError?: boolean } {
|
||||||
|
try {
|
||||||
|
const validatedInput = this.validateThoughtData(input)
|
||||||
|
|
||||||
|
if (validatedInput.thoughtNumber > validatedInput.totalThoughts) {
|
||||||
|
validatedInput.totalThoughts = validatedInput.thoughtNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
this.thoughtHistory.push(validatedInput)
|
||||||
|
|
||||||
|
if (validatedInput.branchFromThought && validatedInput.branchId) {
|
||||||
|
if (!this.branches[validatedInput.branchId]) {
|
||||||
|
this.branches[validatedInput.branchId] = []
|
||||||
|
}
|
||||||
|
this.branches[validatedInput.branchId].push(validatedInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedThought = this.formatThought(validatedInput)
|
||||||
|
console.error(formattedThought)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
thoughtNumber: validatedInput.thoughtNumber,
|
||||||
|
totalThoughts: validatedInput.totalThoughts,
|
||||||
|
nextThoughtNeeded: validatedInput.nextThoughtNeeded,
|
||||||
|
branches: Object.keys(this.branches),
|
||||||
|
thoughtHistoryLength: this.thoughtHistory.length
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
status: 'failed'
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEQUENTIAL_THINKING_TOOL: Tool = {
|
||||||
|
name: 'sequentialthinking',
|
||||||
|
description: `A detailed tool for dynamic and reflective problem-solving through thoughts.
|
||||||
|
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
|
||||||
|
Each thought can build on, question, or revise previous insights as understanding deepens.
|
||||||
|
|
||||||
|
When to use this tool:
|
||||||
|
- Breaking down complex problems into steps
|
||||||
|
- Planning and design with room for revision
|
||||||
|
- Analysis that might need course correction
|
||||||
|
- Problems where the full scope might not be clear initially
|
||||||
|
- Problems that require a multi-step solution
|
||||||
|
- Tasks that need to maintain context over multiple steps
|
||||||
|
- Situations where irrelevant information needs to be filtered out
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- You can adjust total_thoughts up or down as you progress
|
||||||
|
- You can question or revise previous thoughts
|
||||||
|
- You can add more thoughts even after reaching what seemed like the end
|
||||||
|
- You can express uncertainty and explore alternative approaches
|
||||||
|
- Not every thought needs to build linearly - you can branch or backtrack
|
||||||
|
- Generates a solution hypothesis
|
||||||
|
- Verifies the hypothesis based on the Chain of Thought steps
|
||||||
|
- Repeats the process until satisfied
|
||||||
|
- Provides a correct answer
|
||||||
|
|
||||||
|
Parameters explained:
|
||||||
|
- thought: Your current thinking step, which can include:
|
||||||
|
* Regular analytical steps
|
||||||
|
* Revisions of previous thoughts
|
||||||
|
* Questions about previous decisions
|
||||||
|
* Realizations about needing more analysis
|
||||||
|
* Changes in approach
|
||||||
|
* Hypothesis generation
|
||||||
|
* Hypothesis verification
|
||||||
|
- next_thought_needed: True if you need more thinking, even if at what seemed like the end
|
||||||
|
- thought_number: Current number in sequence (can go beyond initial total if needed)
|
||||||
|
- total_thoughts: Current estimate of thoughts needed (can be adjusted up/down)
|
||||||
|
- is_revision: A boolean indicating if this thought revises previous thinking
|
||||||
|
- revises_thought: If is_revision is true, which thought number is being reconsidered
|
||||||
|
- branch_from_thought: If branching, which thought number is the branching point
|
||||||
|
- branch_id: Identifier for the current branch (if any)
|
||||||
|
- needs_more_thoughts: If reaching end but realizing more thoughts needed
|
||||||
|
|
||||||
|
You should:
|
||||||
|
1. Start with an initial estimate of needed thoughts, but be ready to adjust
|
||||||
|
2. Feel free to question or revise previous thoughts
|
||||||
|
3. Don't hesitate to add more thoughts if needed, even at the "end"
|
||||||
|
4. Express uncertainty when present
|
||||||
|
5. Mark thoughts that revise previous thinking or branch into new paths
|
||||||
|
6. Ignore information that is irrelevant to the current step
|
||||||
|
7. Generate a solution hypothesis when appropriate
|
||||||
|
8. Verify the hypothesis based on the Chain of Thought steps
|
||||||
|
9. Repeat the process until satisfied with the solution
|
||||||
|
10. Provide a single, ideally correct answer as the final output
|
||||||
|
11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`,
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
thought: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Your current thinking step'
|
||||||
|
},
|
||||||
|
nextThoughtNeeded: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether another thought step is needed'
|
||||||
|
},
|
||||||
|
thoughtNumber: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Current thought number',
|
||||||
|
minimum: 1
|
||||||
|
},
|
||||||
|
totalThoughts: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Estimated total thoughts needed',
|
||||||
|
minimum: 1
|
||||||
|
},
|
||||||
|
isRevision: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether this revises previous thinking'
|
||||||
|
},
|
||||||
|
revisesThought: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Which thought is being reconsidered',
|
||||||
|
minimum: 1
|
||||||
|
},
|
||||||
|
branchFromThought: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Branching point thought number',
|
||||||
|
minimum: 1
|
||||||
|
},
|
||||||
|
branchId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Branch identifier'
|
||||||
|
},
|
||||||
|
needsMoreThoughts: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'If more thoughts are needed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThinkingServer {
|
||||||
|
public server: Server
|
||||||
|
private thinkingServer: SequentialThinkingServer
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.thinkingServer = new SequentialThinkingServer()
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: 'sequential-thinking-server',
|
||||||
|
version: '0.2.0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: [SEQUENTIAL_THINKING_TOOL]
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
if (request.params.name === 'sequentialthinking') {
|
||||||
|
return this.thinkingServer.processThought(request.params.arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Unknown tool: ${request.params.name}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThinkingServer
|
||||||
@ -2,11 +2,13 @@ import os from 'node:os'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import { isLinux, isMac, isWin } from '@main/constant'
|
import { isLinux, isMac, isWin } from '@main/constant'
|
||||||
|
import { createInMemoryMCPServer } from '@main/mcpServers/factory'
|
||||||
import { makeSureDirExists } from '@main/utils'
|
import { makeSureDirExists } from '@main/utils'
|
||||||
import { getBinaryName, getBinaryPath } from '@main/utils/process'
|
import { getBinaryName, getBinaryPath } from '@main/utils/process'
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||||
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||||
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
import { MCPServer, MCPTool } from '@types'
|
import { MCPServer, MCPTool } from '@types'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
@ -61,11 +63,25 @@ class McpService {
|
|||||||
|
|
||||||
const args = [...(server.args || [])]
|
const args = [...(server.args || [])]
|
||||||
|
|
||||||
let transport: StdioClientTransport | SSEClientTransport
|
let transport: StdioClientTransport | SSEClientTransport | InMemoryTransport
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create appropriate transport based on configuration
|
// Create appropriate transport based on configuration
|
||||||
if (server.baseUrl) {
|
if (server.type === 'inMemory') {
|
||||||
|
Logger.info(`[MCP] Using in-memory transport for server: ${server.name}`)
|
||||||
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
||||||
|
// start the in-memory server with the given name and environment variables
|
||||||
|
const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {})
|
||||||
|
try {
|
||||||
|
await inMemoryServer.connect(serverTransport)
|
||||||
|
Logger.info(`[MCP] In-memory server started: ${server.name}`)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[MCP] Error starting in-memory server: ${error}`)
|
||||||
|
throw new Error(`Failed to start in-memory server: ${error}`)
|
||||||
|
}
|
||||||
|
// set the client transport to the client
|
||||||
|
transport = clientTransport
|
||||||
|
} else if (server.baseUrl) {
|
||||||
transport = new SSEClientTransport(new URL(server.baseUrl))
|
transport = new SSEClientTransport(new URL(server.baseUrl))
|
||||||
} else if (server.command) {
|
} else if (server.command) {
|
||||||
let cmd = server.command
|
let cmd = server.command
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
|
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
|
||||||
@ -74,3 +75,7 @@ export function getTempDir() {
|
|||||||
export function getFilesDir() {
|
export function getFilesDir() {
|
||||||
return path.join(app.getPath('userData'), 'Data', 'Files')
|
return path.join(app.getPath('userData'), 'Data', 'Files')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getConfigDir() {
|
||||||
|
return path.join(os.homedir(), '.cherrystudio', 'config')
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||||
<title>Cherry Studio</title>
|
<title>Cherry Studio</title>
|
||||||
|
|
||||||
|
|||||||
@ -21,8 +21,9 @@
|
|||||||
h6 {
|
h6 {
|
||||||
margin: 1em 0 1em 0;
|
margin: 1em 0 1em 0;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
font-family:
|
||||||
'Helvetica Neue', sans-serif;
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@ -170,8 +171,9 @@
|
|||||||
th {
|
th {
|
||||||
background-color: var(--color-background-mute);
|
background-color: var(--color-background-mute);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
font-family:
|
||||||
'Helvetica Neue', sans-serif;
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|||||||
@ -999,6 +999,9 @@
|
|||||||
"argsTooltip": "Each argument on a new line",
|
"argsTooltip": "Each argument on a new line",
|
||||||
"baseUrlTooltip": "Remote server base URL",
|
"baseUrlTooltip": "Remote server base URL",
|
||||||
"command": "Command",
|
"command": "Command",
|
||||||
|
"sse": "Server-Sent Events(sse)",
|
||||||
|
"stdio": "Standard Input/Output(stdio)",
|
||||||
|
"inMemory": "Memory",
|
||||||
"config_description": "Configure Model Context Protocol servers",
|
"config_description": "Configure Model Context Protocol servers",
|
||||||
"deleteError": "Failed to delete server",
|
"deleteError": "Failed to delete server",
|
||||||
"deleteSuccess": "Server deleted successfully",
|
"deleteSuccess": "Server deleted successfully",
|
||||||
|
|||||||
@ -998,6 +998,9 @@
|
|||||||
"argsTooltip": "1行に1つの引数を入力してください",
|
"argsTooltip": "1行に1つの引数を入力してください",
|
||||||
"baseUrlTooltip": "リモートURLアドレス",
|
"baseUrlTooltip": "リモートURLアドレス",
|
||||||
"command": "コマンド",
|
"command": "コマンド",
|
||||||
|
"sse": "サーバー送信イベント(sse)",
|
||||||
|
"stdio": "標準入力/出力(stdio)",
|
||||||
|
"inMemory": "メモリ",
|
||||||
"config_description": "モデルコンテキストプロトコルサーバーの設定",
|
"config_description": "モデルコンテキストプロトコルサーバーの設定",
|
||||||
"deleteError": "サーバーの削除に失敗しました",
|
"deleteError": "サーバーの削除に失敗しました",
|
||||||
"deleteSuccess": "サーバーが正常に削除されました",
|
"deleteSuccess": "サーバーが正常に削除されました",
|
||||||
|
|||||||
@ -998,6 +998,9 @@
|
|||||||
"argsTooltip": "Каждый аргумент с новой строки",
|
"argsTooltip": "Каждый аргумент с новой строки",
|
||||||
"baseUrlTooltip": "Адрес удаленного URL",
|
"baseUrlTooltip": "Адрес удаленного URL",
|
||||||
"command": "Команда",
|
"command": "Команда",
|
||||||
|
"sse": "События, отправляемые сервером(sse)",
|
||||||
|
"stdio": "Стандартный ввод/вывод(stdio)",
|
||||||
|
"inMemory": "Память",
|
||||||
"config_description": "Настройка серверов протокола контекста модели",
|
"config_description": "Настройка серверов протокола контекста модели",
|
||||||
"deleteError": "Не удалось удалить сервер",
|
"deleteError": "Не удалось удалить сервер",
|
||||||
"deleteSuccess": "Сервер успешно удален",
|
"deleteSuccess": "Сервер успешно удален",
|
||||||
|
|||||||
@ -999,6 +999,9 @@
|
|||||||
"argsTooltip": "每个参数占一行",
|
"argsTooltip": "每个参数占一行",
|
||||||
"baseUrlTooltip": "远程 URL 地址",
|
"baseUrlTooltip": "远程 URL 地址",
|
||||||
"command": "命令",
|
"command": "命令",
|
||||||
|
"sse": "服务器发送事件(sse)",
|
||||||
|
"stdio": "标准输入/输出(stdio)",
|
||||||
|
"inMemory": "内存",
|
||||||
"config_description": "配置模型上下文协议服务器",
|
"config_description": "配置模型上下文协议服务器",
|
||||||
"deleteError": "删除服务器失败",
|
"deleteError": "删除服务器失败",
|
||||||
"deleteSuccess": "服务器删除成功",
|
"deleteSuccess": "服务器删除成功",
|
||||||
|
|||||||
@ -998,6 +998,9 @@
|
|||||||
"argsTooltip": "每個參數佔一行",
|
"argsTooltip": "每個參數佔一行",
|
||||||
"baseUrlTooltip": "遠端 URL 地址",
|
"baseUrlTooltip": "遠端 URL 地址",
|
||||||
"command": "指令",
|
"command": "指令",
|
||||||
|
"sse": "伺服器傳送事件(sse)",
|
||||||
|
"stdio": "標準輸入/輸出(stdio)",
|
||||||
|
"inMemory": "記憶體",
|
||||||
"config_description": "設定模型上下文協議伺服器",
|
"config_description": "設定模型上下文協議伺服器",
|
||||||
"deleteError": "刪除伺服器失敗",
|
"deleteError": "刪除伺服器失敗",
|
||||||
"deleteSuccess": "伺服器刪除成功",
|
"deleteSuccess": "伺服器刪除成功",
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { CodeOutlined } from '@ant-design/icons'
|
import { CodeOutlined } from '@ant-design/icons'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
|
import { initializeMCPServers } from '@renderer/store/mcp'
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPServer } from '@renderer/types'
|
||||||
import { Dropdown, Switch, Tooltip } from 'antd'
|
import { Dropdown, Switch, Tooltip } from 'antd'
|
||||||
import { FC, useRef, useState } from 'react'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -13,16 +15,21 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
||||||
const { activedMcpServers } = useMCPServers()
|
const { activedMcpServers, mcpServers } = useMCPServers()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const dropdownRef = useRef<any>(null)
|
const dropdownRef = useRef<any>(null)
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
|
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
|
||||||
|
|
||||||
const buttonEnabled = availableMCPs.length > 0
|
const buttonEnabled = availableMCPs.length > 0
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeMCPServers(mcpServers, dispatch)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
const truncateText = (text: string, maxLength: number = 50) => {
|
const truncateText = (text: string, maxLength: number = 50) => {
|
||||||
if (!text || text.length <= maxLength) return text
|
if (!text || text.length <= maxLength) return text
|
||||||
return text.substring(0, maxLength) + '...'
|
return text.substring(0, maxLength) + '...'
|
||||||
|
|||||||
@ -17,7 +17,7 @@ interface Props {
|
|||||||
interface MCPFormValues {
|
interface MCPFormValues {
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
serverType: 'sse' | 'stdio'
|
serverType: MCPServer['type']
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
command?: string
|
command?: string
|
||||||
registryUrl?: string
|
registryUrl?: string
|
||||||
@ -42,19 +42,19 @@ const PipRegistry: Registry[] = [
|
|||||||
|
|
||||||
const McpSettings: React.FC<Props> = ({ server }) => {
|
const McpSettings: React.FC<Props> = ({ server }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { deleteMCPServer } = useMCPServers()
|
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
||||||
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio')
|
const [serverType, setServerType] = useState<MCPServer['type']>('stdio')
|
||||||
const [form] = Form.useForm<MCPFormValues>()
|
const [form] = Form.useForm<MCPFormValues>()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [isFormChanged, setIsFormChanged] = useState(false)
|
const [isFormChanged, setIsFormChanged] = useState(false)
|
||||||
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
||||||
const { updateMCPServer } = useMCPServers()
|
|
||||||
const [tools, setTools] = useState<MCPTool[]>([])
|
const [tools, setTools] = useState<MCPTool[]>([])
|
||||||
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
||||||
const [registry, setRegistry] = useState<Registry[]>()
|
const [registry, setRegistry] = useState<Registry[]>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serverType = server.baseUrl ? 'sse' : 'stdio'
|
const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
|
||||||
setServerType(serverType)
|
setServerType(serverType)
|
||||||
|
|
||||||
// Set registry UI state based on command and registryUrl
|
// Set registry UI state based on command and registryUrl
|
||||||
@ -103,7 +103,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [form.getFieldValue('serverType')])
|
}, [form.getFieldValue('serverType')])
|
||||||
|
|
||||||
const fetchTools = useCallback(async () => {
|
const fetchTools = async () => {
|
||||||
if (server.isActive) {
|
if (server.isActive) {
|
||||||
try {
|
try {
|
||||||
setLoadingServer(server.id)
|
setLoadingServer(server.id)
|
||||||
@ -119,15 +119,14 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
setLoadingServer(null)
|
setLoadingServer(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}
|
||||||
}, [server.id])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Loading tools for server:', server.id, 'Active:', server.isActive)
|
|
||||||
if (server.isActive) {
|
if (server.isActive) {
|
||||||
fetchTools()
|
fetchTools()
|
||||||
}
|
}
|
||||||
}, [server.id, server.isActive, fetchTools])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [server.id, server.isActive])
|
||||||
|
|
||||||
// Save the form data
|
// Save the form data
|
||||||
const onSave = async () => {
|
const onSave = async () => {
|
||||||
@ -139,6 +138,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
const mcpServer: MCPServer = {
|
const mcpServer: MCPServer = {
|
||||||
id: server.id,
|
id: server.id,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
|
type: values.serverType,
|
||||||
description: values.description,
|
description: values.description,
|
||||||
isActive: values.isActive,
|
isActive: values.isActive,
|
||||||
registryUrl: values.registryUrl
|
registryUrl: values.registryUrl
|
||||||
@ -171,7 +171,6 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
await window.api.mcp.restartServer(mcpServer)
|
await window.api.mcp.restartServer(mcpServer)
|
||||||
updateMCPServer({ ...mcpServer, isActive: true })
|
updateMCPServer({ ...mcpServer, isActive: true })
|
||||||
window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' })
|
window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' })
|
||||||
await fetchTools()
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setIsFormChanged(false)
|
setIsFormChanged(false)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -312,7 +311,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
<SettingTitle>
|
<SettingTitle>
|
||||||
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
|
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
|
||||||
<ServerName className="text-nowrap">{server?.name}</ServerName>
|
<ServerName className="text-nowrap">{server?.name}</ServerName>
|
||||||
|
{!(server.type === 'inMemory') && (
|
||||||
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
|
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex align="center" gap={16}>
|
<Flex align="center" gap={16}>
|
||||||
<Switch
|
<Switch
|
||||||
@ -347,8 +348,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
<Radio.Group
|
<Radio.Group
|
||||||
onChange={(e) => setServerType(e.target.value)}
|
onChange={(e) => setServerType(e.target.value)}
|
||||||
options={[
|
options={[
|
||||||
{ label: 'STDIO', value: 'stdio' },
|
{ label: t('settings.mcp.stdio'), value: 'stdio' },
|
||||||
{ label: 'SSE', value: 'sse' }
|
{ label: t('settings.mcp.sse'), value: 'sse' },
|
||||||
|
{ label: t('settings.mcp.inMemory'), value: 'inMemory' }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -402,6 +404,17 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
|
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
|
||||||
|
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{serverType === 'inMemory' && (
|
||||||
|
<>
|
||||||
|
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
|
||||||
|
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
|
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
|
||||||
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
|
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@ -8,11 +8,13 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { EventEmitter } from '@renderer/services/EventService'
|
import { EventEmitter } from '@renderer/services/EventService'
|
||||||
|
import { initializeMCPServers } from '@renderer/store/mcp'
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPServer } from '@renderer/types'
|
||||||
import { Dropdown, MenuProps } from 'antd'
|
import { Dropdown, MenuProps } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingContainer } from '..'
|
import { SettingContainer } from '..'
|
||||||
@ -26,6 +28,7 @@ const MCPSettings: FC = () => {
|
|||||||
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
|
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
|
||||||
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubs = [
|
const unsubs = [
|
||||||
@ -35,6 +38,11 @@ const MCPSettings: FC = () => {
|
|||||||
return () => unsubs.forEach((unsub) => unsub())
|
return () => unsubs.forEach((unsub) => unsub())
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeMCPServers(mcpServers, dispatch)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []) // Empty dependency array to run only once
|
||||||
|
|
||||||
const onAddMcpServer = async () => {
|
const onAddMcpServer = async () => {
|
||||||
const newServer = {
|
const newServer = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
|
|||||||
@ -1,21 +1,8 @@
|
|||||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, nanoid, type PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
|
||||||
import type { MCPConfig, MCPServer } from '@renderer/types'
|
import type { MCPConfig, MCPServer } from '@renderer/types'
|
||||||
|
|
||||||
const initialState: MCPConfig = {
|
export const initialState: MCPConfig = {
|
||||||
servers: [
|
servers: []
|
||||||
{
|
|
||||||
id: nanoid(),
|
|
||||||
name: 'mcp-auto-install',
|
|
||||||
description: 'Automatically install MCP services (Beta version)',
|
|
||||||
baseUrl: '',
|
|
||||||
command: 'npx',
|
|
||||||
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
|
|
||||||
registryUrl: 'https://registry.npmmirror.com',
|
|
||||||
env: {},
|
|
||||||
isActive: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mcpSlice = createSlice({
|
const mcpSlice = createSlice({
|
||||||
@ -63,3 +50,79 @@ export const selectMCP = (state: { mcp: MCPConfig }) => state.mcp
|
|||||||
export { mcpSlice }
|
export { mcpSlice }
|
||||||
// Export the reducer as default export
|
// Export the reducer as default export
|
||||||
export default mcpSlice.reducer
|
export default mcpSlice.reducer
|
||||||
|
|
||||||
|
export const builtinMCPServers: MCPServer[] = [
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: '@cherry/mcp-auto-install',
|
||||||
|
description: 'Automatically install MCP services (Beta version)',
|
||||||
|
command: 'npx',
|
||||||
|
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: '@cherry/memory',
|
||||||
|
type: 'inMemory',
|
||||||
|
description:
|
||||||
|
'A basic implementation of persistent memory using a local knowledge graph. This lets Claude remember information about the user across chats. https://github.com/modelcontextprotocol/servers/tree/main/src/memory',
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: '@cherry/sequentialthinking',
|
||||||
|
type: 'inMemory',
|
||||||
|
description:
|
||||||
|
'An MCP server implementation that provides a tool for dynamic and reflective problem-solving through a structured thinking process.',
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: '@cherry/brave-search',
|
||||||
|
type: 'inMemory',
|
||||||
|
description:
|
||||||
|
'An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities.',
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: '@cherry/everything',
|
||||||
|
type: 'inMemory',
|
||||||
|
description:
|
||||||
|
'This MCP server attempts to exercise all the features of the MCP protocol. It is not intended to be a useful server, but rather a test server for builders of MCP clients. It implements prompts, tools, resources, sampling, and more to showcase MCP capabilities.',
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: '@cherry/fetch',
|
||||||
|
type: 'inMemory',
|
||||||
|
description: 'An MCP server for fetching URLs / Youtube video transcript.',
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
name: '@cherry/filesystem',
|
||||||
|
type: 'inMemory',
|
||||||
|
description: 'Node.js server implementing Model Context Protocol (MCP) for filesystem operations.',
|
||||||
|
isActive: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to add servers to the MCP store during app initialization
|
||||||
|
* @param servers Array of MCP servers to add
|
||||||
|
* @param dispatch Redux dispatch function
|
||||||
|
*/
|
||||||
|
export const initializeMCPServers = (existingServers: MCPServer[], dispatch: (action: any) => void): void => {
|
||||||
|
// Check if the existing servers already contain the built-in servers
|
||||||
|
const serverIds = new Set(existingServers.map((server) => server.name))
|
||||||
|
|
||||||
|
// Filter out any built-in servers that are already present
|
||||||
|
const newServers = builtinMCPServers.filter((server) => !serverIds.has(server.name))
|
||||||
|
|
||||||
|
console.log('Adding new servers:', newServers)
|
||||||
|
// Add the new built-in servers to the existing servers
|
||||||
|
newServers.forEach((server) => {
|
||||||
|
dispatch(addMCPServer(server))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -227,6 +227,7 @@ export type AppInfo = {
|
|||||||
version: string
|
version: string
|
||||||
isPackaged: boolean
|
isPackaged: boolean
|
||||||
appPath: string
|
appPath: string
|
||||||
|
configPath: string
|
||||||
appDataPath: string
|
appDataPath: string
|
||||||
resourcesPath: string
|
resourcesPath: string
|
||||||
filesPath: string
|
filesPath: string
|
||||||
@ -365,6 +366,7 @@ export interface MCPServerParameter {
|
|||||||
export interface MCPServer {
|
export interface MCPServer {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
type?: 'stdio' | 'sse' | 'inMemory'
|
||||||
description?: string
|
description?: string
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
command?: string
|
command?: string
|
||||||
|
|||||||
@ -15,9 +15,7 @@ import {
|
|||||||
SimpleStringSchema,
|
SimpleStringSchema,
|
||||||
Tool as geminiTool
|
Tool as geminiTool
|
||||||
} from '@google/generative-ai'
|
} from '@google/generative-ai'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { addMCPServer } from '@renderer/store/mcp'
|
|
||||||
import { MCPServer, MCPTool, MCPToolResponse } from '@renderer/types'
|
import { MCPServer, MCPTool, MCPToolResponse } from '@renderer/types'
|
||||||
import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources'
|
import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources'
|
||||||
|
|
||||||
@ -234,24 +232,6 @@ export async function callMCPTool(tool: MCPTool): Promise<any> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
console.log(`[MCP] Tool called: ${tool.serverName} ${tool.name}`, resp)
|
console.log(`[MCP] Tool called: ${tool.serverName} ${tool.name}`, resp)
|
||||||
|
|
||||||
if (tool.serverName === 'mcp-auto-install') {
|
|
||||||
if (resp.data) {
|
|
||||||
const mcpServer: MCPServer = {
|
|
||||||
id: `f${nanoid()}`,
|
|
||||||
name: resp.data.name,
|
|
||||||
description: resp.data.description,
|
|
||||||
baseUrl: resp.data.baseUrl,
|
|
||||||
command: resp.data.command,
|
|
||||||
args: resp.data.args,
|
|
||||||
env: resp.data.env,
|
|
||||||
registryUrl: '',
|
|
||||||
isActive: false
|
|
||||||
}
|
|
||||||
store.dispatch(addMCPServer(mcpServer))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[MCP] Error calling Tool: ${tool.serverName} ${tool.name}`, e)
|
console.error(`[MCP] Error calling Tool: ${tool.serverName} ${tool.name}`, e)
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Selection Menu</title>
|
<title>Selection Menu</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@ -134,7 +133,7 @@
|
|||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.querySelectorAll('button').forEach(button => {
|
document.querySelectorAll('button').forEach((button) => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const action = button.getAttribute('data-action')
|
const action = button.getAttribute('data-action')
|
||||||
window.api.selectionMenu.action(action)
|
window.api.selectionMenu.action(action)
|
||||||
@ -142,5 +141,4 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
508
yarn.lock
508
yarn.lock
@ -191,6 +191,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@asamuzakjp/css-color@npm:^3.1.1":
|
||||||
|
version: 3.1.1
|
||||||
|
resolution: "@asamuzakjp/css-color@npm:3.1.1"
|
||||||
|
dependencies:
|
||||||
|
"@csstools/css-calc": "npm:^2.1.2"
|
||||||
|
"@csstools/css-color-parser": "npm:^3.0.8"
|
||||||
|
"@csstools/css-parser-algorithms": "npm:^3.0.4"
|
||||||
|
"@csstools/css-tokenizer": "npm:^3.0.3"
|
||||||
|
lru-cache: "npm:^10.4.3"
|
||||||
|
checksum: 10c0/4abb010fd29de8acae8571eba738468c22cb45a1f77647df3c59a80f1c83d83d728cae3ebbf99e5c73f2517761abaaffbe5e4176fc46b5f9bf60f1478463b51e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@babel/code-frame@npm:^7.26.2":
|
"@babel/code-frame@npm:^7.26.2":
|
||||||
version: 7.26.2
|
version: 7.26.2
|
||||||
resolution: "@babel/code-frame@npm:7.26.2"
|
resolution: "@babel/code-frame@npm:7.26.2"
|
||||||
@ -632,6 +645,52 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@csstools/color-helpers@npm:^5.0.2":
|
||||||
|
version: 5.0.2
|
||||||
|
resolution: "@csstools/color-helpers@npm:5.0.2"
|
||||||
|
checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@csstools/css-calc@npm:^2.1.2":
|
||||||
|
version: 2.1.2
|
||||||
|
resolution: "@csstools/css-calc@npm:2.1.2"
|
||||||
|
peerDependencies:
|
||||||
|
"@csstools/css-parser-algorithms": ^3.0.4
|
||||||
|
"@csstools/css-tokenizer": ^3.0.3
|
||||||
|
checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@csstools/css-color-parser@npm:^3.0.8":
|
||||||
|
version: 3.0.8
|
||||||
|
resolution: "@csstools/css-color-parser@npm:3.0.8"
|
||||||
|
dependencies:
|
||||||
|
"@csstools/color-helpers": "npm:^5.0.2"
|
||||||
|
"@csstools/css-calc": "npm:^2.1.2"
|
||||||
|
peerDependencies:
|
||||||
|
"@csstools/css-parser-algorithms": ^3.0.4
|
||||||
|
"@csstools/css-tokenizer": ^3.0.3
|
||||||
|
checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@csstools/css-parser-algorithms@npm:^3.0.4":
|
||||||
|
version: 3.0.4
|
||||||
|
resolution: "@csstools/css-parser-algorithms@npm:3.0.4"
|
||||||
|
peerDependencies:
|
||||||
|
"@csstools/css-tokenizer": ^3.0.3
|
||||||
|
checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@csstools/css-tokenizer@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "@csstools/css-tokenizer@npm:3.0.3"
|
||||||
|
checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@develar/schema-utils@npm:~2.6.5":
|
"@develar/schema-utils@npm:~2.6.5":
|
||||||
version: 2.6.5
|
version: 2.6.5
|
||||||
resolution: "@develar/schema-utils@npm:2.6.5"
|
resolution: "@develar/schema-utils@npm:2.6.5"
|
||||||
@ -2369,6 +2428,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@mixmark-io/domino@npm:^2.2.0":
|
||||||
|
version: 2.2.0
|
||||||
|
resolution: "@mixmark-io/domino@npm:2.2.0"
|
||||||
|
checksum: 10c0/aa468a15f9217d425220fe6a4b3f9416cbe8e566ee14efc191c6d5cc04fe39338b16a90bbac190f28d44e69465db5f2cf95f479c621ce38060ca6b2a3d346e9d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk@npm:^1.8.0":
|
"@modelcontextprotocol/sdk@npm:^1.8.0":
|
||||||
version: 1.8.0
|
version: 1.8.0
|
||||||
resolution: "@modelcontextprotocol/sdk@npm:1.8.0"
|
resolution: "@modelcontextprotocol/sdk@npm:1.8.0"
|
||||||
@ -2922,6 +2988,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@sec-ant/readable-stream@npm:^0.4.1":
|
||||||
|
version: 0.4.1
|
||||||
|
resolution: "@sec-ant/readable-stream@npm:0.4.1"
|
||||||
|
checksum: 10c0/64e9e9cf161e848067a5bf60cdc04d18495dc28bb63a8d9f8993e4dd99b91ad34e4b563c85de17d91ffb177ec17a0664991d2e115f6543e73236a906068987af
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@selderee/plugin-htmlparser2@npm:^0.11.0":
|
"@selderee/plugin-htmlparser2@npm:^0.11.0":
|
||||||
version: 0.11.0
|
version: 0.11.0
|
||||||
resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
|
resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
|
||||||
@ -3002,20 +3075,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@sindresorhus/is@npm:^4.0.0":
|
"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.2.0":
|
||||||
version: 4.6.0
|
version: 4.6.0
|
||||||
resolution: "@sindresorhus/is@npm:4.6.0"
|
resolution: "@sindresorhus/is@npm:4.6.0"
|
||||||
checksum: 10c0/33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e
|
checksum: 10c0/33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@sindresorhus/is@npm:^5.2.0":
|
"@sindresorhus/is@npm:^5.2.0, @sindresorhus/is@npm:^5.3.0":
|
||||||
version: 5.6.0
|
version: 5.6.0
|
||||||
resolution: "@sindresorhus/is@npm:5.6.0"
|
resolution: "@sindresorhus/is@npm:5.6.0"
|
||||||
checksum: 10c0/66727344d0c92edde5760b5fd1f8092b717f2298a162a5f7f29e4953e001479927402d9d387e245fb9dc7d3b37c72e335e93ed5875edfc5203c53be8ecba1b52
|
checksum: 10c0/66727344d0c92edde5760b5fd1f8092b717f2298a162a5f7f29e4953e001479927402d9d387e245fb9dc7d3b37c72e335e93ed5875edfc5203c53be8ecba1b52
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@sindresorhus/is@npm:^7.0.1":
|
||||||
|
version: 7.0.1
|
||||||
|
resolution: "@sindresorhus/is@npm:7.0.1"
|
||||||
|
checksum: 10c0/6d43a916d70d9b64066394c272883869b22faf21f4748aaf399c1b691ea704ea607d1668ff2eb5704e5be8809c4a7faafe16be048ce5e1a2ba6e8928b8e3461c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@szmarczak/http-timer@npm:^4.0.5":
|
"@szmarczak/http-timer@npm:^4.0.5":
|
||||||
version: 4.0.6
|
version: 4.0.6
|
||||||
resolution: "@szmarczak/http-timer@npm:4.0.6"
|
resolution: "@szmarczak/http-timer@npm:4.0.6"
|
||||||
@ -3226,6 +3306,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/diff@npm:^7":
|
||||||
|
version: 7.0.2
|
||||||
|
resolution: "@types/diff@npm:7.0.2"
|
||||||
|
checksum: 10c0/ac4de3f982242292e006ace98a9d41363ebc244145939466139828ffa6c476acc15eea2bad39bd7e0868003c497614f6d7e734d4999c4f09d95dfd173d24d723
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/estree-jsx@npm:^1.0.0":
|
"@types/estree-jsx@npm:^1.0.0":
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
resolution: "@types/estree-jsx@npm:1.0.5"
|
resolution: "@types/estree-jsx@npm:1.0.5"
|
||||||
@ -3280,7 +3367,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2":
|
"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2, @types/http-cache-semantics@npm:^4.0.4":
|
||||||
version: 4.0.4
|
version: 4.0.4
|
||||||
resolution: "@types/http-cache-semantics@npm:4.0.4"
|
resolution: "@types/http-cache-semantics@npm:4.0.4"
|
||||||
checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6
|
checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6
|
||||||
@ -3777,6 +3864,7 @@ __metadata:
|
|||||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
|
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
|
||||||
"@tryfabric/martian": "npm:^1.2.4"
|
"@tryfabric/martian": "npm:^1.2.4"
|
||||||
"@types/adm-zip": "npm:^0"
|
"@types/adm-zip": "npm:^0"
|
||||||
|
"@types/diff": "npm:^7"
|
||||||
"@types/fs-extra": "npm:^11"
|
"@types/fs-extra": "npm:^11"
|
||||||
"@types/lodash": "npm:^4.17.5"
|
"@types/lodash": "npm:^4.17.5"
|
||||||
"@types/markdown-it": "npm:^14"
|
"@types/markdown-it": "npm:^14"
|
||||||
@ -3798,6 +3886,7 @@ __metadata:
|
|||||||
dayjs: "npm:^1.11.11"
|
dayjs: "npm:^1.11.11"
|
||||||
dexie: "npm:^4.0.8"
|
dexie: "npm:^4.0.8"
|
||||||
dexie-react-hooks: "npm:^1.1.7"
|
dexie-react-hooks: "npm:^1.1.7"
|
||||||
|
diff: "npm:^7.0.0"
|
||||||
docx: "npm:^9.0.2"
|
docx: "npm:^9.0.2"
|
||||||
dotenv-cli: "npm:^7.4.2"
|
dotenv-cli: "npm:^7.4.2"
|
||||||
electron: "npm:31.7.6"
|
electron: "npm:31.7.6"
|
||||||
@ -3819,9 +3908,11 @@ __metadata:
|
|||||||
fast-xml-parser: "npm:^5.0.9"
|
fast-xml-parser: "npm:^5.0.9"
|
||||||
fetch-socks: "npm:^1.3.2"
|
fetch-socks: "npm:^1.3.2"
|
||||||
fs-extra: "npm:^11.2.0"
|
fs-extra: "npm:^11.2.0"
|
||||||
|
got-scraping: "npm:^4.1.1"
|
||||||
html-to-image: "npm:^1.11.13"
|
html-to-image: "npm:^1.11.13"
|
||||||
husky: "npm:^9.1.7"
|
husky: "npm:^9.1.7"
|
||||||
i18next: "npm:^23.11.5"
|
i18next: "npm:^23.11.5"
|
||||||
|
jsdom: "npm:^26.0.0"
|
||||||
lint-staged: "npm:^15.5.0"
|
lint-staged: "npm:^15.5.0"
|
||||||
lodash: "npm:^4.17.21"
|
lodash: "npm:^4.17.21"
|
||||||
markdown-it: "npm:^14.1.0"
|
markdown-it: "npm:^14.1.0"
|
||||||
@ -3859,6 +3950,8 @@ __metadata:
|
|||||||
tar: "npm:^7.4.3"
|
tar: "npm:^7.4.3"
|
||||||
tinycolor2: "npm:^1.6.0"
|
tinycolor2: "npm:^1.6.0"
|
||||||
tokenx: "npm:^0.4.1"
|
tokenx: "npm:^0.4.1"
|
||||||
|
turndown: "npm:^7.2.0"
|
||||||
|
turndown-plugin-gfm: "npm:^1.0.2"
|
||||||
typescript: "npm:^5.6.2"
|
typescript: "npm:^5.6.2"
|
||||||
undici: "npm:^7.4.0"
|
undici: "npm:^7.4.0"
|
||||||
uuid: "npm:^10.0.0"
|
uuid: "npm:^10.0.0"
|
||||||
@ -3926,7 +4019,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"adm-zip@npm:^0.5.16":
|
"adm-zip@npm:^0.5.16, adm-zip@npm:^0.5.9":
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
resolution: "adm-zip@npm:0.5.16"
|
resolution: "adm-zip@npm:0.5.16"
|
||||||
checksum: 10c0/6f10119d4570c7ba76dcf428abb8d3f69e63f92e51f700a542b43d4c0130373dd2ddfc8f85059f12d4a843703a90c3970cfd17876844b4f3f48bf042bfa6b49f
|
checksum: 10c0/6f10119d4570c7ba76dcf428abb8d3f69e63f92e51f700a542b43d4c0130373dd2ddfc8f85059f12d4a843703a90c3970cfd17876844b4f3f48bf042bfa6b49f
|
||||||
@ -4565,7 +4658,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"browserslist@npm:^4.24.0":
|
"browserslist@npm:^4.21.1, browserslist@npm:^4.24.0":
|
||||||
version: 4.24.4
|
version: 4.24.4
|
||||||
resolution: "browserslist@npm:4.24.4"
|
resolution: "browserslist@npm:4.24.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4798,6 +4891,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cacheable-request@npm:^12.0.1":
|
||||||
|
version: 12.0.1
|
||||||
|
resolution: "cacheable-request@npm:12.0.1"
|
||||||
|
dependencies:
|
||||||
|
"@types/http-cache-semantics": "npm:^4.0.4"
|
||||||
|
get-stream: "npm:^9.0.1"
|
||||||
|
http-cache-semantics: "npm:^4.1.1"
|
||||||
|
keyv: "npm:^4.5.4"
|
||||||
|
mimic-response: "npm:^4.0.0"
|
||||||
|
normalize-url: "npm:^8.0.1"
|
||||||
|
responselike: "npm:^3.0.0"
|
||||||
|
checksum: 10c0/3ccc26519c8dd0821fcb21fa00781e55f05ab6e1da1487fbbee9c8c03435a3cf72c29a710a991cebe398fb9a5274e2a772fc488546d402db8dc21310764ed83a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"cacheable-request@npm:^7.0.2":
|
"cacheable-request@npm:^7.0.2":
|
||||||
version: 7.0.4
|
version: 7.0.4
|
||||||
resolution: "cacheable-request@npm:7.0.4"
|
resolution: "cacheable-request@npm:7.0.4"
|
||||||
@ -4833,13 +4941,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"callsites@npm:^3.0.0":
|
"callsites@npm:^3.0.0, callsites@npm:^3.1.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "callsites@npm:3.1.0"
|
resolution: "callsites@npm:3.1.0"
|
||||||
checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301
|
checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"callsites@npm:^4.0.0":
|
||||||
|
version: 4.2.0
|
||||||
|
resolution: "callsites@npm:4.2.0"
|
||||||
|
checksum: 10c0/8f7e269ec09fc0946bb22d838a8bc7932e1909ab4a833b964749f4d0e8bdeaa1f253287c4f911f61781f09620b6925ccd19a5ea4897489c4e59442c660c312a3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"camelcase@npm:5.0.0":
|
"camelcase@npm:5.0.0":
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
resolution: "camelcase@npm:5.0.0"
|
resolution: "camelcase@npm:5.0.0"
|
||||||
@ -5435,6 +5550,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cssstyle@npm:^4.2.1":
|
||||||
|
version: 4.3.0
|
||||||
|
resolution: "cssstyle@npm:4.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@asamuzakjp/css-color": "npm:^3.1.1"
|
||||||
|
rrweb-cssom: "npm:^0.8.0"
|
||||||
|
checksum: 10c0/770ccb288a99257fd0d5b129e03878f848e922d3b017358acb02e8dd530e8f0c7c6f74e6ae5367d715e2da36a490a734b4177fc1b78f3f08eca25f204a56a692
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"csstype@npm:3.1.3, csstype@npm:^3.0.2, csstype@npm:^3.1.3":
|
"csstype@npm:3.1.3, csstype@npm:^3.0.2, csstype@npm:^3.1.3":
|
||||||
version: 3.1.3
|
version: 3.1.3
|
||||||
resolution: "csstype@npm:3.1.3"
|
resolution: "csstype@npm:3.1.3"
|
||||||
@ -5563,6 +5688,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"data-urls@npm:^5.0.0":
|
||||||
|
version: 5.0.0
|
||||||
|
resolution: "data-urls@npm:5.0.0"
|
||||||
|
dependencies:
|
||||||
|
whatwg-mimetype: "npm:^4.0.0"
|
||||||
|
whatwg-url: "npm:^14.0.0"
|
||||||
|
checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"dayjs@npm:^1.11.11":
|
"dayjs@npm:^1.11.11":
|
||||||
version: 1.11.13
|
version: 1.11.13
|
||||||
resolution: "dayjs@npm:1.11.13"
|
resolution: "dayjs@npm:1.11.13"
|
||||||
@ -5628,6 +5763,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"decimal.js@npm:^10.4.3":
|
||||||
|
version: 10.5.0
|
||||||
|
resolution: "decimal.js@npm:10.5.0"
|
||||||
|
checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"decode-named-character-reference@npm:^1.0.0":
|
"decode-named-character-reference@npm:^1.0.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "decode-named-character-reference@npm:1.1.0"
|
resolution: "decode-named-character-reference@npm:1.1.0"
|
||||||
@ -5904,6 +6046,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"diff@npm:^7.0.0":
|
||||||
|
version: 7.0.0
|
||||||
|
resolution: "diff@npm:7.0.0"
|
||||||
|
checksum: 10c0/251fd15f85ffdf814cfc35a728d526b8d2ad3de338dcbd011ac6e57c461417090766b28995f8ff733135b5fbc3699c392db1d5e27711ac4e00244768cd1d577b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"dingbat-to-unicode@npm:^1.0.1":
|
"dingbat-to-unicode@npm:^1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "dingbat-to-unicode@npm:1.0.1"
|
resolution: "dingbat-to-unicode@npm:1.0.1"
|
||||||
@ -6034,6 +6183,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dot-prop@npm:^7.2.0":
|
||||||
|
version: 7.2.0
|
||||||
|
resolution: "dot-prop@npm:7.2.0"
|
||||||
|
dependencies:
|
||||||
|
type-fest: "npm:^2.11.2"
|
||||||
|
checksum: 10c0/2621702a01e7a47730e3a8e2938a406afc79b62fbb77bd1394e786ff13776673904bf0a4fc6b812eb9849ec71034e9fc1019a9e0bbe91f84010d8a8088cd41a9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"dotenv-cli@npm:^7.4.2":
|
"dotenv-cli@npm:^7.4.2":
|
||||||
version: 7.4.4
|
version: 7.4.4
|
||||||
resolution: "dotenv-cli@npm:7.4.4"
|
resolution: "dotenv-cli@npm:7.4.4"
|
||||||
@ -7516,7 +7674,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"form-data@npm:^4.0.0":
|
"form-data-encoder@npm:^4.0.2":
|
||||||
|
version: 4.0.2
|
||||||
|
resolution: "form-data-encoder@npm:4.0.2"
|
||||||
|
checksum: 10c0/559d3130e265316452434eaf68d68560fb36392ff4d04614683419de4fb43c3dbe152dc303599fae382ce24d3451a6d3d289d3bcc182ae3d8ad32e7ce8e35e53
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"form-data@npm:^4.0.0, form-data@npm:^4.0.1":
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
resolution: "form-data@npm:4.0.2"
|
resolution: "form-data@npm:4.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7758,6 +7923,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"generative-bayesian-network@npm:^2.1.63":
|
||||||
|
version: 2.1.63
|
||||||
|
resolution: "generative-bayesian-network@npm:2.1.63"
|
||||||
|
dependencies:
|
||||||
|
adm-zip: "npm:^0.5.9"
|
||||||
|
tslib: "npm:^2.4.0"
|
||||||
|
checksum: 10c0/d0b886663f14f7b8a43ea7f03fdac4e5d83f692a7829c5ed6af2ac6777b30b1c560dc1c55525bd7f50b0d0bf6ad28109151e85741b6f8dd4f0eb87f0dce42ea8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"gensync@npm:^1.0.0-beta.2":
|
"gensync@npm:^1.0.0-beta.2":
|
||||||
version: 1.0.0-beta.2
|
version: 1.0.0-beta.2
|
||||||
resolution: "gensync@npm:1.0.0-beta.2"
|
resolution: "gensync@npm:1.0.0-beta.2"
|
||||||
@ -7847,6 +8022,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"get-stream@npm:^9.0.1":
|
||||||
|
version: 9.0.1
|
||||||
|
resolution: "get-stream@npm:9.0.1"
|
||||||
|
dependencies:
|
||||||
|
"@sec-ant/readable-stream": "npm:^0.4.1"
|
||||||
|
is-stream: "npm:^4.0.1"
|
||||||
|
checksum: 10c0/d70e73857f2eea1826ac570c3a912757dcfbe8a718a033fa0c23e12ac8e7d633195b01710e0559af574cbb5af101009b42df7b6f6b29ceec8dbdf7291931b948
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"get-uri@npm:^6.0.1":
|
"get-uri@npm:^6.0.1":
|
||||||
version: 6.0.4
|
version: 6.0.4
|
||||||
resolution: "get-uri@npm:6.0.4"
|
resolution: "get-uri@npm:6.0.4"
|
||||||
@ -8029,6 +8214,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"got-scraping@npm:^4.1.1":
|
||||||
|
version: 4.1.1
|
||||||
|
resolution: "got-scraping@npm:4.1.1"
|
||||||
|
dependencies:
|
||||||
|
got: "npm:^14.2.1"
|
||||||
|
header-generator: "npm:^2.1.41"
|
||||||
|
http2-wrapper: "npm:^2.2.0"
|
||||||
|
mimic-response: "npm:^4.0.0"
|
||||||
|
ow: "npm:^1.1.1"
|
||||||
|
quick-lru: "npm:^7.0.0"
|
||||||
|
tslib: "npm:^2.6.2"
|
||||||
|
checksum: 10c0/66b9bd88fea1c7a1248fec6e9c9757300b70e6039d2b2e0cf1c70e44e88be80f02a26e2e36d5f9c3acb4ec963558d72b0d236a7f11a7a6c87b39b5615afcf7db
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"got@npm:13.0.0":
|
"got@npm:13.0.0":
|
||||||
version: 13.0.0
|
version: 13.0.0
|
||||||
resolution: "got@npm:13.0.0"
|
resolution: "got@npm:13.0.0"
|
||||||
@ -8067,6 +8267,25 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"got@npm:^14.2.1":
|
||||||
|
version: 14.4.7
|
||||||
|
resolution: "got@npm:14.4.7"
|
||||||
|
dependencies:
|
||||||
|
"@sindresorhus/is": "npm:^7.0.1"
|
||||||
|
"@szmarczak/http-timer": "npm:^5.0.1"
|
||||||
|
cacheable-lookup: "npm:^7.0.0"
|
||||||
|
cacheable-request: "npm:^12.0.1"
|
||||||
|
decompress-response: "npm:^6.0.0"
|
||||||
|
form-data-encoder: "npm:^4.0.2"
|
||||||
|
http2-wrapper: "npm:^2.2.1"
|
||||||
|
lowercase-keys: "npm:^3.0.0"
|
||||||
|
p-cancelable: "npm:^4.0.1"
|
||||||
|
responselike: "npm:^3.0.0"
|
||||||
|
type-fest: "npm:^4.26.1"
|
||||||
|
checksum: 10c0/9b5b8dbc0642c78dbc64ab5ff6f12f6edab3e0cb80e89a3a69623a79ba3986f0ff0066a116fba47c0aacce4b0ba1eccf72f923f7fac13a31ce852bf9e2cb8f81
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
|
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
|
||||||
version: 4.2.11
|
version: 4.2.11
|
||||||
resolution: "graceful-fs@npm:4.2.11"
|
resolution: "graceful-fs@npm:4.2.11"
|
||||||
@ -8366,6 +8585,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"header-generator@npm:^2.1.41":
|
||||||
|
version: 2.1.63
|
||||||
|
resolution: "header-generator@npm:2.1.63"
|
||||||
|
dependencies:
|
||||||
|
browserslist: "npm:^4.21.1"
|
||||||
|
generative-bayesian-network: "npm:^2.1.63"
|
||||||
|
ow: "npm:^0.28.1"
|
||||||
|
tslib: "npm:^2.4.0"
|
||||||
|
checksum: 10c0/a6f49019d77df53dfaa0f6e4ad7f7b99a8ee5d598eeed5956d0adbd32a22846d2c7dfb4e912cdc0a06eb954b7c39a285bd1b329e479ae9d6f122e6c544425297
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
|
"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
resolution: "hoist-non-react-statics@npm:3.3.2"
|
resolution: "hoist-non-react-statics@npm:3.3.2"
|
||||||
@ -8398,6 +8629,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"html-encoding-sniffer@npm:^4.0.0":
|
||||||
|
version: 4.0.0
|
||||||
|
resolution: "html-encoding-sniffer@npm:4.0.0"
|
||||||
|
dependencies:
|
||||||
|
whatwg-encoding: "npm:^3.1.1"
|
||||||
|
checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"html-parse-stringify@npm:^3.0.1":
|
"html-parse-stringify@npm:^3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "html-parse-stringify@npm:3.0.1"
|
resolution: "html-parse-stringify@npm:3.0.1"
|
||||||
@ -8495,7 +8735,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1":
|
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1, http-proxy-agent@npm:^7.0.2":
|
||||||
version: 7.0.2
|
version: 7.0.2
|
||||||
resolution: "http-proxy-agent@npm:7.0.2"
|
resolution: "http-proxy-agent@npm:7.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8526,7 +8766,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"http2-wrapper@npm:^2.1.10":
|
"http2-wrapper@npm:^2.1.10, http2-wrapper@npm:^2.2.0, http2-wrapper@npm:^2.2.1":
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
resolution: "http2-wrapper@npm:2.2.1"
|
resolution: "http2-wrapper@npm:2.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9012,6 +9252,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"is-potential-custom-element-name@npm:^1.0.1":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "is-potential-custom-element-name@npm:1.0.1"
|
||||||
|
checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"is-promise@npm:^4.0.0":
|
"is-promise@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "is-promise@npm:4.0.0"
|
resolution: "is-promise@npm:4.0.0"
|
||||||
@ -9040,6 +9287,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"is-stream@npm:^4.0.1":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "is-stream@npm:4.0.1"
|
||||||
|
checksum: 10c0/2706c7f19b851327ba374687bc4a3940805e14ca496dc672b9629e744d143b1ad9c6f1b162dece81c7bfbc0f83b32b61ccc19ad2e05aad2dd7af347408f60c7f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"is-typedarray@npm:~1.0.0":
|
"is-typedarray@npm:~1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "is-typedarray@npm:1.0.0"
|
resolution: "is-typedarray@npm:1.0.0"
|
||||||
@ -9200,6 +9454,40 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jsdom@npm:^26.0.0":
|
||||||
|
version: 26.0.0
|
||||||
|
resolution: "jsdom@npm:26.0.0"
|
||||||
|
dependencies:
|
||||||
|
cssstyle: "npm:^4.2.1"
|
||||||
|
data-urls: "npm:^5.0.0"
|
||||||
|
decimal.js: "npm:^10.4.3"
|
||||||
|
form-data: "npm:^4.0.1"
|
||||||
|
html-encoding-sniffer: "npm:^4.0.0"
|
||||||
|
http-proxy-agent: "npm:^7.0.2"
|
||||||
|
https-proxy-agent: "npm:^7.0.6"
|
||||||
|
is-potential-custom-element-name: "npm:^1.0.1"
|
||||||
|
nwsapi: "npm:^2.2.16"
|
||||||
|
parse5: "npm:^7.2.1"
|
||||||
|
rrweb-cssom: "npm:^0.8.0"
|
||||||
|
saxes: "npm:^6.0.0"
|
||||||
|
symbol-tree: "npm:^3.2.4"
|
||||||
|
tough-cookie: "npm:^5.0.0"
|
||||||
|
w3c-xmlserializer: "npm:^5.0.0"
|
||||||
|
webidl-conversions: "npm:^7.0.0"
|
||||||
|
whatwg-encoding: "npm:^3.1.1"
|
||||||
|
whatwg-mimetype: "npm:^4.0.0"
|
||||||
|
whatwg-url: "npm:^14.1.0"
|
||||||
|
ws: "npm:^8.18.0"
|
||||||
|
xml-name-validator: "npm:^5.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
canvas: ^3.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
canvas:
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/e48725ba4027edcfc9bca5799eaec72c6561ecffe3675a8ff87fe9c3541ca4ff9f82b4eff5b3d9c527302da0d859b2f60e9364347a5d42b77f5c76c436c569dc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jsesc@npm:^3.0.2":
|
"jsesc@npm:^3.0.2":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "jsesc@npm:3.1.0"
|
resolution: "jsesc@npm:3.1.0"
|
||||||
@ -9801,7 +10089,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3":
|
||||||
version: 10.4.3
|
version: 10.4.3
|
||||||
resolution: "lru-cache@npm:10.4.3"
|
resolution: "lru-cache@npm:10.4.3"
|
||||||
checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb
|
checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb
|
||||||
@ -11533,7 +11821,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"normalize-url@npm:^8.0.0":
|
"normalize-url@npm:^8.0.0, normalize-url@npm:^8.0.1":
|
||||||
version: 8.0.1
|
version: 8.0.1
|
||||||
resolution: "normalize-url@npm:8.0.1"
|
resolution: "normalize-url@npm:8.0.1"
|
||||||
checksum: 10c0/eb439231c4b84430f187530e6fdac605c5048ef4ec556447a10c00a91fc69b52d8d8298d9d608e68d3e0f7dc2d812d3455edf425e0f215993667c3183bcab1ef
|
checksum: 10c0/eb439231c4b84430f187530e6fdac605c5048ef4ec556447a10c00a91fc69b52d8d8298d9d608e68d3e0f7dc2d812d3455edf425e0f215993667c3183bcab1ef
|
||||||
@ -11614,6 +11902,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"nwsapi@npm:^2.2.16":
|
||||||
|
version: 2.2.20
|
||||||
|
resolution: "nwsapi@npm:2.2.20"
|
||||||
|
checksum: 10c0/07f4dafa3186aef7c007863e90acd4342a34ba9d44b22f14f644fdb311f6086887e21c2fc15efaa826c2bc39ab2bc841364a1a630e7c87e0cb723ba59d729297
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"oauth-sign@npm:~0.9.0":
|
"oauth-sign@npm:~0.9.0":
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
resolution: "oauth-sign@npm:0.9.0"
|
resolution: "oauth-sign@npm:0.9.0"
|
||||||
@ -11887,6 +12182,32 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ow@npm:^0.28.1":
|
||||||
|
version: 0.28.2
|
||||||
|
resolution: "ow@npm:0.28.2"
|
||||||
|
dependencies:
|
||||||
|
"@sindresorhus/is": "npm:^4.2.0"
|
||||||
|
callsites: "npm:^3.1.0"
|
||||||
|
dot-prop: "npm:^6.0.1"
|
||||||
|
lodash.isequal: "npm:^4.5.0"
|
||||||
|
vali-date: "npm:^1.0.0"
|
||||||
|
checksum: 10c0/8d0de10fd3aa1ab69dd844ace087718c31ceb1a25cf79d38a5be4d0a5da46f960b6bc15a95405747899b882fb51dcf5a502d7e6508005d1c57e157d12fa17cdd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"ow@npm:^1.1.1":
|
||||||
|
version: 1.1.1
|
||||||
|
resolution: "ow@npm:1.1.1"
|
||||||
|
dependencies:
|
||||||
|
"@sindresorhus/is": "npm:^5.3.0"
|
||||||
|
callsites: "npm:^4.0.0"
|
||||||
|
dot-prop: "npm:^7.2.0"
|
||||||
|
lodash.isequal: "npm:^4.5.0"
|
||||||
|
vali-date: "npm:^1.0.0"
|
||||||
|
checksum: 10c0/3973f9d6245f2e468a0f1d614ece96f1289632f7425094e8b266b50ddbe79471f2e6cba447b80e90b54bbeb13c20e83671edfb5ef4c0b13c15546ba0710554e1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"p-cancelable@npm:^2.0.0":
|
"p-cancelable@npm:^2.0.0":
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
resolution: "p-cancelable@npm:2.1.1"
|
resolution: "p-cancelable@npm:2.1.1"
|
||||||
@ -11901,6 +12222,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"p-cancelable@npm:^4.0.1":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "p-cancelable@npm:4.0.1"
|
||||||
|
checksum: 10c0/12636623f46784ba962b6fe7a1f34d021f1d9a2cc12c43e270baa715ea872d5c8c7d9f086ed420b8b9817e91d9bbe92c14c90e5dddd4a9968c81a2a7aef7089d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"p-finally@npm:^1.0.0":
|
"p-finally@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "p-finally@npm:1.0.0"
|
resolution: "p-finally@npm:1.0.0"
|
||||||
@ -12138,7 +12466,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"parse5@npm:^7.0.0":
|
"parse5@npm:^7.0.0, parse5@npm:^7.2.1":
|
||||||
version: 7.2.1
|
version: 7.2.1
|
||||||
resolution: "parse5@npm:7.2.1"
|
resolution: "parse5@npm:7.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -12734,6 +13062,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"quick-lru@npm:^7.0.0":
|
||||||
|
version: 7.0.0
|
||||||
|
resolution: "quick-lru@npm:7.0.0"
|
||||||
|
checksum: 10c0/d46e3dc6d2f79f83d3dc852b32b3d9fcf5337bf5b4016122c1b729aa7bcd54dd79b2f3cb36b59a5a3f5c30443ecfbc9e7d400cfc30feedd8d58d3d4213d08a18
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"raf-schd@npm:^4.0.3":
|
"raf-schd@npm:^4.0.3":
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
resolution: "raf-schd@npm:4.0.3"
|
resolution: "raf-schd@npm:4.0.3"
|
||||||
@ -14139,6 +14474,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"rrweb-cssom@npm:^0.8.0":
|
||||||
|
version: 0.8.0
|
||||||
|
resolution: "rrweb-cssom@npm:0.8.0"
|
||||||
|
checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"run-parallel@npm:^1.1.9":
|
"run-parallel@npm:^1.1.9":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "run-parallel@npm:1.2.0"
|
resolution: "run-parallel@npm:1.2.0"
|
||||||
@ -14202,6 +14544,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"saxes@npm:^6.0.0":
|
||||||
|
version: 6.0.0
|
||||||
|
resolution: "saxes@npm:6.0.0"
|
||||||
|
dependencies:
|
||||||
|
xmlchars: "npm:^2.2.0"
|
||||||
|
checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"scheduler@npm:^0.25.0":
|
"scheduler@npm:^0.25.0":
|
||||||
version: 0.25.0
|
version: 0.25.0
|
||||||
resolution: "scheduler@npm:0.25.0"
|
resolution: "scheduler@npm:0.25.0"
|
||||||
@ -15045,6 +15396,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"symbol-tree@npm:^3.2.4":
|
||||||
|
version: 3.2.4
|
||||||
|
resolution: "symbol-tree@npm:3.2.4"
|
||||||
|
checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"synckit@npm:^0.9.1":
|
"synckit@npm:^0.9.1":
|
||||||
version: 0.9.2
|
version: 0.9.2
|
||||||
resolution: "synckit@npm:0.9.2"
|
resolution: "synckit@npm:0.9.2"
|
||||||
@ -15220,6 +15578,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tldts-core@npm:^6.1.85":
|
||||||
|
version: 6.1.85
|
||||||
|
resolution: "tldts-core@npm:6.1.85"
|
||||||
|
checksum: 10c0/f028759b361bef86d3dd8dbaaa010b4c3aca236ec7ba097658b9a595243bb34c98206e410cc3631055af6ed34012f5da7e9af2c4e38e218d5fc693df6ab3da33
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"tldts@npm:^6.1.32":
|
||||||
|
version: 6.1.85
|
||||||
|
resolution: "tldts@npm:6.1.85"
|
||||||
|
dependencies:
|
||||||
|
tldts-core: "npm:^6.1.85"
|
||||||
|
bin:
|
||||||
|
tldts: bin/cli.js
|
||||||
|
checksum: 10c0/83bc222046f36a9ca071b662e3272bb1e98e849fa4bddfab0a3eba8804a4a539b02bcbe55e1277fe713de6677e55104da2ef9a54d006c642b6e9f26c7595d5aa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tmp-promise@npm:^3.0.2":
|
"tmp-promise@npm:^3.0.2":
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
resolution: "tmp-promise@npm:3.0.3"
|
resolution: "tmp-promise@npm:3.0.3"
|
||||||
@ -15293,6 +15669,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tough-cookie@npm:^5.0.0":
|
||||||
|
version: 5.1.2
|
||||||
|
resolution: "tough-cookie@npm:5.1.2"
|
||||||
|
dependencies:
|
||||||
|
tldts: "npm:^6.1.32"
|
||||||
|
checksum: 10c0/5f95023a47de0f30a902bba951664b359725597d8adeabc66a0b93a931c3af801e1e697dae4b8c21a012056c0ea88bd2bf4dfe66b2adcf8e2f42cd9796fe0626
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tough-cookie@npm:~2.5.0":
|
"tough-cookie@npm:~2.5.0":
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
resolution: "tough-cookie@npm:2.5.0"
|
resolution: "tough-cookie@npm:2.5.0"
|
||||||
@ -15303,6 +15688,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tr46@npm:^5.1.0":
|
||||||
|
version: 5.1.0
|
||||||
|
resolution: "tr46@npm:5.1.0"
|
||||||
|
dependencies:
|
||||||
|
punycode: "npm:^2.3.1"
|
||||||
|
checksum: 10c0/d761f7144e0cb296187674ef245c74f761e334d7cf25ca73ef60e4c72c097c75051031c093fa1a2fee04b904977b316716a7915854bcae8fb1a371746513cbe8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tr46@npm:~0.0.3":
|
"tr46@npm:~0.0.3":
|
||||||
version: 0.0.3
|
version: 0.0.3
|
||||||
resolution: "tr46@npm:0.0.3"
|
resolution: "tr46@npm:0.0.3"
|
||||||
@ -15363,7 +15757,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.6.2":
|
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2":
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
resolution: "tslib@npm:2.8.1"
|
resolution: "tslib@npm:2.8.1"
|
||||||
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
|
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
|
||||||
@ -15379,6 +15773,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"turndown-plugin-gfm@npm:^1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "turndown-plugin-gfm@npm:1.0.2"
|
||||||
|
checksum: 10c0/eb9bc20dbb08d5335231f9617d7440f14b35781f14a3a393d8f13fc8205afeb11a0a632d52da4548ab0fa353f315ca265462b24d368faf23258dccbe439182b9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"turndown@npm:^7.2.0":
|
||||||
|
version: 7.2.0
|
||||||
|
resolution: "turndown@npm:7.2.0"
|
||||||
|
dependencies:
|
||||||
|
"@mixmark-io/domino": "npm:^2.2.0"
|
||||||
|
checksum: 10c0/6abcdcdf9d35cd79d7a8100a7de1d2226b921d5bd99e73ac14a7ead39c059978f519378913375efb04c68bcfc40f7ffe2dee0ce9ae4d54dc1235b12856a78d4e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0":
|
"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0":
|
||||||
version: 0.14.5
|
version: 0.14.5
|
||||||
resolution: "tweetnacl@npm:0.14.5"
|
resolution: "tweetnacl@npm:0.14.5"
|
||||||
@ -15402,13 +15812,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"type-fest@npm:^2.17.0":
|
"type-fest@npm:^2.11.2, type-fest@npm:^2.17.0":
|
||||||
version: 2.19.0
|
version: 2.19.0
|
||||||
resolution: "type-fest@npm:2.19.0"
|
resolution: "type-fest@npm:2.19.0"
|
||||||
checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb
|
checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"type-fest@npm:^4.26.1":
|
||||||
|
version: 4.39.1
|
||||||
|
resolution: "type-fest@npm:4.39.1"
|
||||||
|
checksum: 10c0/f5bf302eb2e2f70658be1757aa578f4a09da3f65699b0b12b7ae5502ccea76e5124521a6e6b69540f442c3dc924c394202a2ab58718d0582725c7ac23c072594
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"type-fest@npm:^4.37.0":
|
"type-fest@npm:^4.37.0":
|
||||||
version: 4.37.0
|
version: 4.37.0
|
||||||
resolution: "type-fest@npm:4.37.0"
|
resolution: "type-fest@npm:4.37.0"
|
||||||
@ -15860,6 +16277,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"vali-date@npm:^1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "vali-date@npm:1.0.0"
|
||||||
|
checksum: 10c0/5755215f6734caab535f60af0a32bbbf2052c61b1a40668d773df78fd3754e4fe9da2ea5466731505f3e0a599acc209d5578c4b70488ed120fb03f0c2ab06449
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"validate-npm-package-license@npm:^3.0.1":
|
"validate-npm-package-license@npm:^3.0.1":
|
||||||
version: 3.0.4
|
version: 3.0.4
|
||||||
resolution: "validate-npm-package-license@npm:3.0.4"
|
resolution: "validate-npm-package-license@npm:3.0.4"
|
||||||
@ -16001,6 +16425,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"w3c-xmlserializer@npm:^5.0.0":
|
||||||
|
version: 5.0.0
|
||||||
|
resolution: "w3c-xmlserializer@npm:5.0.0"
|
||||||
|
dependencies:
|
||||||
|
xml-name-validator: "npm:^5.0.0"
|
||||||
|
checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"web-namespaces@npm:^2.0.0":
|
"web-namespaces@npm:^2.0.0":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "web-namespaces@npm:2.0.1"
|
resolution: "web-namespaces@npm:2.0.1"
|
||||||
@ -16051,6 +16484,39 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"webidl-conversions@npm:^7.0.0":
|
||||||
|
version: 7.0.0
|
||||||
|
resolution: "webidl-conversions@npm:7.0.0"
|
||||||
|
checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"whatwg-encoding@npm:^3.1.1":
|
||||||
|
version: 3.1.1
|
||||||
|
resolution: "whatwg-encoding@npm:3.1.1"
|
||||||
|
dependencies:
|
||||||
|
iconv-lite: "npm:0.6.3"
|
||||||
|
checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"whatwg-mimetype@npm:^4.0.0":
|
||||||
|
version: 4.0.0
|
||||||
|
resolution: "whatwg-mimetype@npm:4.0.0"
|
||||||
|
checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0":
|
||||||
|
version: 14.2.0
|
||||||
|
resolution: "whatwg-url@npm:14.2.0"
|
||||||
|
dependencies:
|
||||||
|
tr46: "npm:^5.1.0"
|
||||||
|
webidl-conversions: "npm:^7.0.0"
|
||||||
|
checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"whatwg-url@npm:^5.0.0":
|
"whatwg-url@npm:^5.0.0":
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
resolution: "whatwg-url@npm:5.0.0"
|
resolution: "whatwg-url@npm:5.0.0"
|
||||||
@ -16221,6 +16687,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"xml-name-validator@npm:^5.0.0":
|
||||||
|
version: 5.0.0
|
||||||
|
resolution: "xml-name-validator@npm:5.0.0"
|
||||||
|
checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"xml-parse-from-string@npm:^1.0.0":
|
"xml-parse-from-string@npm:^1.0.0":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "xml-parse-from-string@npm:1.0.1"
|
resolution: "xml-parse-from-string@npm:1.0.1"
|
||||||
@ -16286,6 +16759,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"xmlchars@npm:^2.2.0":
|
||||||
|
version: 2.2.0
|
||||||
|
resolution: "xmlchars@npm:2.2.0"
|
||||||
|
checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"xmldom-sre@npm:0.1.31":
|
"xmldom-sre@npm:0.1.31":
|
||||||
version: 0.1.31
|
version: 0.1.31
|
||||||
resolution: "xmldom-sre@npm:0.1.31"
|
resolution: "xmldom-sre@npm:0.1.31"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user