pse-mcp 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-package/package.json +1 -1
- package/package.json +1 -1
- package/dist/google-search.js +0 -235
- package/dist/services/google-search.service.js +0 -244
- package/dist/types.js +0 -1
package/package.json
CHANGED
package/dist/google-search.js
DELETED
@@ -1,235 +0,0 @@
|
|
1
|
-
#!/usr/bin/env node
|
2
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
3
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
5
|
-
import { GoogleSearchService } from './services/google-search.service.js';
|
6
|
-
class GoogleSearchServer {
|
7
|
-
constructor() {
|
8
|
-
this.searchService = new GoogleSearchService();
|
9
|
-
this.server = new Server({
|
10
|
-
name: 'google-search',
|
11
|
-
version: '1.0.0'
|
12
|
-
}, {
|
13
|
-
capabilities: {
|
14
|
-
tools: {
|
15
|
-
google_search: {
|
16
|
-
description: 'Search Google and return relevant results from the web. This tool finds web pages, articles, and information on specific topics using Google\'s search engine. Results include titles, snippets, and URLs that can be analyzed further using extract_webpage_content.',
|
17
|
-
inputSchema: {
|
18
|
-
type: 'object',
|
19
|
-
properties: {
|
20
|
-
query: {
|
21
|
-
type: 'string',
|
22
|
-
description: 'Search query - be specific and use quotes for exact matches. For best results, use clear keywords and avoid very long queries.'
|
23
|
-
},
|
24
|
-
num_results: {
|
25
|
-
type: 'number',
|
26
|
-
description: 'Number of results to return (default: 10, max: 10). Increase for broader coverage, decrease for faster response.'
|
27
|
-
},
|
28
|
-
site: {
|
29
|
-
type: 'string',
|
30
|
-
description: 'Limit search results to a specific website domain (e.g., "wikipedia.org" or "nytimes.com").'
|
31
|
-
},
|
32
|
-
language: {
|
33
|
-
type: 'string',
|
34
|
-
description: 'Filter results by language using ISO 639-1 codes (e.g., "en" for English, "es" for Spanish, "fr" for French).'
|
35
|
-
},
|
36
|
-
dateRestrict: {
|
37
|
-
type: 'string',
|
38
|
-
description: 'Filter results by date using Google\'s date restriction format: "d[number]" for past days, "w[number]" for past weeks, "m[number]" for past months, or "y[number]" for past years. Example: "m6" for results from the past 6 months.'
|
39
|
-
},
|
40
|
-
exactTerms: {
|
41
|
-
type: 'string',
|
42
|
-
description: 'Search for results that contain this exact phrase. This is equivalent to putting the terms in quotes in the search query.'
|
43
|
-
},
|
44
|
-
resultType: {
|
45
|
-
type: 'string',
|
46
|
-
description: 'Specify the type of results to return. Options include "image" (or "images"), "news", and "video" (or "videos"). Default is general web results.'
|
47
|
-
},
|
48
|
-
page: {
|
49
|
-
type: 'number',
|
50
|
-
description: 'Page number for paginated results (starts at 1). Use in combination with resultsPerPage to navigate through large result sets.'
|
51
|
-
},
|
52
|
-
resultsPerPage: {
|
53
|
-
type: 'number',
|
54
|
-
description: 'Number of results to show per page (default: 10, max: 10). Controls how many results are returned for each page.'
|
55
|
-
},
|
56
|
-
sort: {
|
57
|
-
type: 'string',
|
58
|
-
description: 'Sorting method for search results. Options: "relevance" (default) or "date" (most recent first).'
|
59
|
-
}
|
60
|
-
},
|
61
|
-
required: ['query']
|
62
|
-
}
|
63
|
-
}
|
64
|
-
}
|
65
|
-
}
|
66
|
-
});
|
67
|
-
// Register tool list handler
|
68
|
-
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
69
|
-
tools: [
|
70
|
-
{
|
71
|
-
name: 'google_search',
|
72
|
-
description: 'Search Google and return relevant results from the web. This tool finds web pages, articles, and information on specific topics using Google\'s search engine. Results include titles, snippets, and URLs that can be analyzed further using extract_webpage_content.',
|
73
|
-
inputSchema: {
|
74
|
-
type: 'object',
|
75
|
-
properties: {
|
76
|
-
query: {
|
77
|
-
type: 'string',
|
78
|
-
description: 'Search query - be specific and use quotes for exact matches. For best results, use clear keywords and avoid very long queries.'
|
79
|
-
},
|
80
|
-
num_results: {
|
81
|
-
type: 'number',
|
82
|
-
description: 'Number of results to return (default: 10, max: 10). Increase for broader coverage, decrease for faster response.'
|
83
|
-
},
|
84
|
-
site: {
|
85
|
-
type: 'string',
|
86
|
-
description: 'Limit search results to a specific website domain (e.g., "wikipedia.org" or "nytimes.com").'
|
87
|
-
},
|
88
|
-
language: {
|
89
|
-
type: 'string',
|
90
|
-
description: 'Filter results by language using ISO 639-1 codes (e.g., "en" for English, "es" for Spanish, "fr" for French).'
|
91
|
-
},
|
92
|
-
dateRestrict: {
|
93
|
-
type: 'string',
|
94
|
-
description: 'Filter results by date using Google\'s date restriction format: "d[number]" for past days, "w[number]" for past weeks, "m[number]" for past months, or "y[number]" for past years. Example: "m6" for results from the past 6 months.'
|
95
|
-
},
|
96
|
-
exactTerms: {
|
97
|
-
type: 'string',
|
98
|
-
description: 'Search for results that contain this exact phrase. This is equivalent to putting the terms in quotes in the search query.'
|
99
|
-
},
|
100
|
-
resultType: {
|
101
|
-
type: 'string',
|
102
|
-
description: 'Specify the type of results to return. Options include "image" (or "images"), "news", and "video" (or "videos"). Default is general web results.'
|
103
|
-
},
|
104
|
-
page: {
|
105
|
-
type: 'number',
|
106
|
-
description: 'Page number for paginated results (starts at 1). Use in combination with resultsPerPage to navigate through large result sets.'
|
107
|
-
},
|
108
|
-
resultsPerPage: {
|
109
|
-
type: 'number',
|
110
|
-
description: 'Number of results to show per page (default: 10, max: 10). Controls how many results are returned for each page.'
|
111
|
-
},
|
112
|
-
sort: {
|
113
|
-
type: 'string',
|
114
|
-
description: 'Sorting method for search results. Options: "relevance" (default) or "date" (most recent first).'
|
115
|
-
}
|
116
|
-
},
|
117
|
-
required: ['query']
|
118
|
-
}
|
119
|
-
}
|
120
|
-
]
|
121
|
-
}));
|
122
|
-
// Register tool call handler
|
123
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
124
|
-
switch (request.params.name) {
|
125
|
-
case 'google_search':
|
126
|
-
if (typeof request.params.arguments === 'object' && request.params.arguments !== null && 'query' in request.params.arguments) {
|
127
|
-
return this.handleSearch({
|
128
|
-
query: String(request.params.arguments.query),
|
129
|
-
num_results: typeof request.params.arguments.num_results === 'number' ? request.params.arguments.num_results : undefined,
|
130
|
-
filters: {
|
131
|
-
site: request.params.arguments.site ? String(request.params.arguments.site) : undefined,
|
132
|
-
language: request.params.arguments.language ? String(request.params.arguments.language) : undefined,
|
133
|
-
dateRestrict: request.params.arguments.dateRestrict ? String(request.params.arguments.dateRestrict) : undefined,
|
134
|
-
exactTerms: request.params.arguments.exactTerms ? String(request.params.arguments.exactTerms) : undefined,
|
135
|
-
resultType: request.params.arguments.resultType ? String(request.params.arguments.resultType) : undefined,
|
136
|
-
page: typeof request.params.arguments.page === 'number' ? request.params.arguments.page : undefined,
|
137
|
-
resultsPerPage: typeof request.params.arguments.resultsPerPage === 'number' ? request.params.arguments.resultsPerPage : undefined,
|
138
|
-
sort: request.params.arguments.sort ? String(request.params.arguments.sort) : undefined
|
139
|
-
}
|
140
|
-
});
|
141
|
-
}
|
142
|
-
throw new Error('Invalid arguments for google_search tool');
|
143
|
-
default:
|
144
|
-
throw new Error(`Unknown tool: ${request.params.name}`);
|
145
|
-
}
|
146
|
-
});
|
147
|
-
}
|
148
|
-
async handleSearch(args) {
|
149
|
-
try {
|
150
|
-
const { results, pagination, categories } = await this.searchService.search(args.query, args.num_results, args.filters);
|
151
|
-
if (results.length === 0) {
|
152
|
-
return {
|
153
|
-
content: [{
|
154
|
-
type: 'text',
|
155
|
-
text: 'No results found. Try:\n- Using different keywords\n- Removing quotes from non-exact phrases\n- Using more general terms'
|
156
|
-
}],
|
157
|
-
isError: true
|
158
|
-
};
|
159
|
-
}
|
160
|
-
// Format results in a more concise, readable way
|
161
|
-
const formattedResults = results.map(result => ({
|
162
|
-
title: result.title,
|
163
|
-
link: result.link,
|
164
|
-
snippet: result.snippet,
|
165
|
-
category: result.category
|
166
|
-
}));
|
167
|
-
// Format results in a more AI-friendly way
|
168
|
-
let responseText = `Search results for "${args.query}":\n\n`;
|
169
|
-
// Add category summary if available
|
170
|
-
if (categories && categories.length > 0) {
|
171
|
-
responseText += "Categories: " + categories.map(c => `${c.name} (${c.count})`).join(', ') + "\n\n";
|
172
|
-
}
|
173
|
-
// Add pagination info
|
174
|
-
if (pagination) {
|
175
|
-
responseText += `Showing page ${pagination.currentPage}${pagination.totalResults ? ` of approximately ${pagination.totalResults} results` : ''}\n\n`;
|
176
|
-
}
|
177
|
-
// Add each result in a readable format
|
178
|
-
formattedResults.forEach((result, index) => {
|
179
|
-
responseText += `${index + 1}. ${result.title}\n`;
|
180
|
-
responseText += ` URL: ${result.link}\n`;
|
181
|
-
responseText += ` ${result.snippet}\n\n`;
|
182
|
-
});
|
183
|
-
// Add navigation hints if pagination exists
|
184
|
-
if (pagination && (pagination.hasNextPage || pagination.hasPreviousPage)) {
|
185
|
-
responseText += "Navigation: ";
|
186
|
-
if (pagination.hasPreviousPage) {
|
187
|
-
responseText += "Use 'page: " + (pagination.currentPage - 1) + "' for previous results. ";
|
188
|
-
}
|
189
|
-
if (pagination.hasNextPage) {
|
190
|
-
responseText += "Use 'page: " + (pagination.currentPage + 1) + "' for more results.";
|
191
|
-
}
|
192
|
-
responseText += "\n";
|
193
|
-
}
|
194
|
-
return {
|
195
|
-
content: [
|
196
|
-
{
|
197
|
-
type: 'text',
|
198
|
-
text: responseText,
|
199
|
-
},
|
200
|
-
],
|
201
|
-
};
|
202
|
-
}
|
203
|
-
catch (error) {
|
204
|
-
const message = error instanceof Error ? error.message : 'Unknown error during search';
|
205
|
-
return {
|
206
|
-
content: [{ type: 'text', text: message }],
|
207
|
-
isError: true
|
208
|
-
};
|
209
|
-
}
|
210
|
-
}
|
211
|
-
async start() {
|
212
|
-
try {
|
213
|
-
const transport = new StdioServerTransport();
|
214
|
-
await this.server.connect(transport);
|
215
|
-
console.error('Google Search MCP server running');
|
216
|
-
// Keep the process running
|
217
|
-
process.on('SIGINT', () => {
|
218
|
-
this.server.close().catch(console.error);
|
219
|
-
process.exit(0);
|
220
|
-
});
|
221
|
-
}
|
222
|
-
catch (error) {
|
223
|
-
if (error instanceof Error) {
|
224
|
-
console.error('Failed to start MCP server:', error.message);
|
225
|
-
}
|
226
|
-
else {
|
227
|
-
console.error('Failed to start MCP server: Unknown error');
|
228
|
-
}
|
229
|
-
process.exit(1);
|
230
|
-
}
|
231
|
-
}
|
232
|
-
}
|
233
|
-
// Start the server
|
234
|
-
const server = new GoogleSearchServer();
|
235
|
-
server.start().catch(console.error);
|
@@ -1,244 +0,0 @@
|
|
1
|
-
import { google } from 'googleapis';
|
2
|
-
import { URL } from 'url';
|
3
|
-
export class GoogleSearchService {
|
4
|
-
constructor() {
|
5
|
-
// Cache for search results (key: query string + filters, value: results)
|
6
|
-
this.searchCache = new Map();
|
7
|
-
// Cache expiration time in milliseconds (5 minutes)
|
8
|
-
this.cacheTTL = 5 * 60 * 1000;
|
9
|
-
const apiKey = process.env.GOOGLE_API_KEY;
|
10
|
-
const searchEngineId = process.env.GOOGLE_SEARCH_ENGINE_ID;
|
11
|
-
if (!apiKey || !searchEngineId) {
|
12
|
-
throw new Error('Missing required environment variables: GOOGLE_API_KEY and GOOGLE_SEARCH_ENGINE_ID');
|
13
|
-
}
|
14
|
-
// Initialize Google Custom Search API
|
15
|
-
this.customSearch = google.customsearch('v1').cse;
|
16
|
-
this.searchEngineId = searchEngineId;
|
17
|
-
// Set up the API client
|
18
|
-
google.options({
|
19
|
-
auth: apiKey
|
20
|
-
});
|
21
|
-
}
|
22
|
-
/**
|
23
|
-
* Generate a cache key from search parameters
|
24
|
-
*/
|
25
|
-
generateCacheKey(query, numResults, filters) {
|
26
|
-
return JSON.stringify({
|
27
|
-
query,
|
28
|
-
numResults,
|
29
|
-
filters
|
30
|
-
});
|
31
|
-
}
|
32
|
-
/**
|
33
|
-
* Check if a cache entry is still valid
|
34
|
-
*/
|
35
|
-
isCacheValid(entry) {
|
36
|
-
const now = Date.now();
|
37
|
-
return now - entry.timestamp < this.cacheTTL;
|
38
|
-
}
|
39
|
-
/**
|
40
|
-
* Store search results in cache
|
41
|
-
*/
|
42
|
-
cacheSearchResults(cacheKey, results, pagination, categories) {
|
43
|
-
this.searchCache.set(cacheKey, {
|
44
|
-
timestamp: Date.now(),
|
45
|
-
data: { results, pagination, categories }
|
46
|
-
});
|
47
|
-
// Limit cache size to prevent memory issues (max 100 entries)
|
48
|
-
if (this.searchCache.size > 100) {
|
49
|
-
// Delete oldest entry
|
50
|
-
const oldestKey = Array.from(this.searchCache.entries())
|
51
|
-
.sort((a, b) => a[1].timestamp - b[1].timestamp)[0][0];
|
52
|
-
this.searchCache.delete(oldestKey);
|
53
|
-
}
|
54
|
-
}
|
55
|
-
async search(query, numResults = 10, filters) {
|
56
|
-
try {
|
57
|
-
// Generate cache key
|
58
|
-
const cacheKey = this.generateCacheKey(query, numResults, filters);
|
59
|
-
// Check cache first
|
60
|
-
const cachedResult = this.searchCache.get(cacheKey);
|
61
|
-
if (cachedResult && this.isCacheValid(cachedResult)) {
|
62
|
-
console.error('Using cached search results');
|
63
|
-
return cachedResult.data;
|
64
|
-
}
|
65
|
-
let formattedQuery = query;
|
66
|
-
// Apply site filter if provided
|
67
|
-
if (filters?.site) {
|
68
|
-
formattedQuery += ` site:${filters.site}`;
|
69
|
-
}
|
70
|
-
// Apply exact terms if provided
|
71
|
-
if (filters?.exactTerms) {
|
72
|
-
formattedQuery += ` "${filters.exactTerms}"`;
|
73
|
-
}
|
74
|
-
// Set default pagination values if not provided
|
75
|
-
const page = filters?.page && filters.page > 0 ? filters.page : 1;
|
76
|
-
const resultsPerPage = filters?.resultsPerPage ? Math.min(filters.resultsPerPage, 10) : Math.min(numResults, 10);
|
77
|
-
// Calculate start index for pagination (Google uses 1-based indexing)
|
78
|
-
const startIndex = (page - 1) * resultsPerPage + 1;
|
79
|
-
const params = {
|
80
|
-
cx: this.searchEngineId,
|
81
|
-
q: formattedQuery,
|
82
|
-
num: resultsPerPage,
|
83
|
-
start: startIndex
|
84
|
-
};
|
85
|
-
// Apply language filter if provided
|
86
|
-
if (filters?.language) {
|
87
|
-
params.lr = `lang_${filters.language}`;
|
88
|
-
}
|
89
|
-
// Apply date restriction if provided
|
90
|
-
if (filters?.dateRestrict) {
|
91
|
-
params.dateRestrict = filters.dateRestrict;
|
92
|
-
}
|
93
|
-
// Apply result type filter if provided
|
94
|
-
if (filters?.resultType) {
|
95
|
-
switch (filters.resultType.toLowerCase()) {
|
96
|
-
case 'image':
|
97
|
-
case 'images':
|
98
|
-
params.searchType = 'image';
|
99
|
-
break;
|
100
|
-
case 'news':
|
101
|
-
// For news, we need to modify the query
|
102
|
-
formattedQuery += ' source:news';
|
103
|
-
params.q = formattedQuery;
|
104
|
-
break;
|
105
|
-
case 'video':
|
106
|
-
case 'videos':
|
107
|
-
// For videos, we can use a more specific filter
|
108
|
-
formattedQuery += ' filetype:video OR inurl:video OR inurl:watch';
|
109
|
-
params.q = formattedQuery;
|
110
|
-
break;
|
111
|
-
}
|
112
|
-
}
|
113
|
-
// Apply sorting if provided
|
114
|
-
if (filters?.sort) {
|
115
|
-
switch (filters.sort.toLowerCase()) {
|
116
|
-
case 'date':
|
117
|
-
// Sort by date (most recent first)
|
118
|
-
params.sort = 'date';
|
119
|
-
break;
|
120
|
-
case 'relevance':
|
121
|
-
default:
|
122
|
-
// Google's default sort is by relevance, so we don't need to specify
|
123
|
-
break;
|
124
|
-
}
|
125
|
-
}
|
126
|
-
const response = await this.customSearch.list(params);
|
127
|
-
// If no items are found, return empty results with pagination info
|
128
|
-
if (!response.data.items) {
|
129
|
-
return {
|
130
|
-
results: [],
|
131
|
-
pagination: {
|
132
|
-
currentPage: page,
|
133
|
-
resultsPerPage,
|
134
|
-
totalResults: 0,
|
135
|
-
totalPages: 0,
|
136
|
-
hasNextPage: false,
|
137
|
-
hasPreviousPage: page > 1
|
138
|
-
},
|
139
|
-
categories: []
|
140
|
-
};
|
141
|
-
}
|
142
|
-
// Map the search results and categorize them
|
143
|
-
const results = response.data.items.map(item => {
|
144
|
-
const result = {
|
145
|
-
title: item.title || '',
|
146
|
-
link: item.link || '',
|
147
|
-
snippet: item.snippet || '',
|
148
|
-
pagemap: item.pagemap || {},
|
149
|
-
datePublished: item.pagemap?.metatags?.[0]?.['article:published_time'] || '',
|
150
|
-
source: 'google_search'
|
151
|
-
};
|
152
|
-
// Add category to the result
|
153
|
-
result.category = this.categorizeResult(result);
|
154
|
-
return result;
|
155
|
-
});
|
156
|
-
// Generate category statistics
|
157
|
-
const categories = this.generateCategoryStats(results);
|
158
|
-
// Create pagination information
|
159
|
-
const totalResults = parseInt(response.data.searchInformation?.totalResults || '0', 10);
|
160
|
-
const totalPages = Math.ceil(totalResults / resultsPerPage);
|
161
|
-
const pagination = {
|
162
|
-
currentPage: page,
|
163
|
-
resultsPerPage,
|
164
|
-
totalResults,
|
165
|
-
totalPages,
|
166
|
-
hasNextPage: page < totalPages,
|
167
|
-
hasPreviousPage: page > 1
|
168
|
-
};
|
169
|
-
// Cache the results before returning
|
170
|
-
this.cacheSearchResults(cacheKey, results, pagination, categories);
|
171
|
-
return {
|
172
|
-
results,
|
173
|
-
pagination,
|
174
|
-
categories
|
175
|
-
};
|
176
|
-
}
|
177
|
-
catch (error) {
|
178
|
-
if (error instanceof Error) {
|
179
|
-
throw new Error(`Google Search API error: ${error.message}`);
|
180
|
-
}
|
181
|
-
throw new Error('Unknown error during Google search');
|
182
|
-
}
|
183
|
-
}
|
184
|
-
/**
|
185
|
-
* Categorizes a search result based on its content
|
186
|
-
* @param result The search result to categorize
|
187
|
-
* @returns The category name
|
188
|
-
*/
|
189
|
-
categorizeResult(result) {
|
190
|
-
try {
|
191
|
-
// Extract the domain from the URL
|
192
|
-
const url = new URL(result.link);
|
193
|
-
const domain = url.hostname.replace(/^www\./, '');
|
194
|
-
// Check if this is a social media site
|
195
|
-
if (domain.match(/facebook\.com|twitter\.com|instagram\.com|linkedin\.com|pinterest\.com|tiktok\.com|reddit\.com/i)) {
|
196
|
-
return 'Social Media';
|
197
|
-
}
|
198
|
-
// Check if this is a video site
|
199
|
-
if (domain.match(/youtube\.com|vimeo\.com|dailymotion\.com|twitch\.tv/i)) {
|
200
|
-
return 'Video';
|
201
|
-
}
|
202
|
-
// Check if this is a news site
|
203
|
-
if (domain.match(/news|cnn\.com|bbc\.com|nytimes\.com|wsj\.com|reuters\.com|bloomberg\.com/i)) {
|
204
|
-
return 'News';
|
205
|
-
}
|
206
|
-
// Check if this is an educational site
|
207
|
-
if (domain.match(/\.edu$|wikipedia\.org|khan|course|learn|study|academic/i)) {
|
208
|
-
return 'Educational';
|
209
|
-
}
|
210
|
-
// Check if this is a documentation site
|
211
|
-
if (domain.match(/docs|documentation|developer|github\.com|gitlab\.com|bitbucket\.org|stackoverflow\.com/i) ||
|
212
|
-
result.title.match(/docs|documentation|api|reference|manual/i)) {
|
213
|
-
return 'Documentation';
|
214
|
-
}
|
215
|
-
// Check if this is a shopping site
|
216
|
-
if (domain.match(/amazon\.com|ebay\.com|etsy\.com|walmart\.com|shop|store|buy/i)) {
|
217
|
-
return 'Shopping';
|
218
|
-
}
|
219
|
-
// Default category based on domain
|
220
|
-
return domain.split('.').slice(-2, -1)[0].charAt(0).toUpperCase() + domain.split('.').slice(-2, -1)[0].slice(1);
|
221
|
-
}
|
222
|
-
catch (error) {
|
223
|
-
// If there's any error in categorization, return a default category
|
224
|
-
return 'Other';
|
225
|
-
}
|
226
|
-
}
|
227
|
-
/**
|
228
|
-
* Generates category statistics from search results
|
229
|
-
* @param results The search results to analyze
|
230
|
-
* @returns An array of category information
|
231
|
-
*/
|
232
|
-
generateCategoryStats(results) {
|
233
|
-
// Count results by category
|
234
|
-
const categoryCounts = {};
|
235
|
-
results.forEach(result => {
|
236
|
-
const category = result.category || 'Other';
|
237
|
-
categoryCounts[category] = (categoryCounts[category] || 0) + 1;
|
238
|
-
});
|
239
|
-
// Convert to array of category info objects
|
240
|
-
return Object.entries(categoryCounts)
|
241
|
-
.map(([name, count]) => ({ name, count }))
|
242
|
-
.sort((a, b) => b.count - a.count); // Sort by count in descending order
|
243
|
-
}
|
244
|
-
}
|
package/dist/types.js
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export {};
|