@gotza02/sequential-thinking 2026.2.39 → 2026.2.41
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/README.md +7 -4
- package/SYSTEM_INSTRUCTION.md +25 -50
- package/dist/tools/sports/core/base.d.ts +169 -0
- package/dist/tools/sports/core/base.js +289 -0
- package/dist/tools/sports/core/cache.d.ts +106 -0
- package/dist/tools/sports/core/cache.js +305 -0
- package/dist/tools/sports/core/constants.d.ts +179 -0
- package/dist/tools/sports/core/constants.js +149 -0
- package/dist/tools/sports/core/types.d.ts +379 -0
- package/dist/tools/sports/core/types.js +5 -0
- package/dist/tools/sports/index.d.ts +34 -0
- package/dist/tools/sports/index.js +50 -0
- package/dist/tools/sports/providers/api.d.ts +73 -0
- package/dist/tools/sports/providers/api.js +517 -0
- package/dist/tools/sports/providers/scraper.d.ts +66 -0
- package/dist/tools/sports/providers/scraper.js +186 -0
- package/dist/tools/sports/providers/search.d.ts +54 -0
- package/dist/tools/sports/providers/search.js +224 -0
- package/dist/tools/sports/tools/betting.d.ts +6 -0
- package/dist/tools/sports/tools/betting.js +251 -0
- package/dist/tools/sports/tools/league.d.ts +11 -0
- package/dist/tools/sports/tools/league.js +12 -0
- package/dist/tools/sports/tools/live.d.ts +9 -0
- package/dist/tools/sports/tools/live.js +235 -0
- package/dist/tools/sports/tools/match.d.ts +6 -0
- package/dist/tools/sports/tools/match.js +323 -0
- package/dist/tools/sports/tools/player.d.ts +6 -0
- package/dist/tools/sports/tools/player.js +152 -0
- package/dist/tools/sports/tools/team.d.ts +6 -0
- package/dist/tools/sports/tools/team.js +370 -0
- package/dist/tools/sports/utils/calculator.d.ts +69 -0
- package/dist/tools/sports/utils/calculator.js +156 -0
- package/dist/tools/sports/utils/formatter.d.ts +57 -0
- package/dist/tools/sports/utils/formatter.js +206 -0
- package/dist/tools/sports.d.ts +7 -0
- package/dist/tools/sports.js +27 -6
- package/package.json +1 -1
- package/system_instruction.md +155 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPORTS MODULE SCRAPER PROVIDER
|
|
3
|
+
* Web scraping provider for sports data from various websites
|
|
4
|
+
*/
|
|
5
|
+
import { ScraperProviderBase } from '../core/base.js';
|
|
6
|
+
import { SCRAPER_CONFIG } from '../core/constants.js';
|
|
7
|
+
import { fetchWithRetry } from '../../../utils.js';
|
|
8
|
+
import { JSDOM } from 'jsdom';
|
|
9
|
+
import { Readability } from '@mozilla/readability';
|
|
10
|
+
import TurndownService from 'turndown';
|
|
11
|
+
/**
|
|
12
|
+
* Web Scraper Provider for sports data
|
|
13
|
+
*/
|
|
14
|
+
export class ScraperProvider extends ScraperProviderBase {
|
|
15
|
+
turndown;
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.turndown = new TurndownService({
|
|
19
|
+
headingStyle: 'atx',
|
|
20
|
+
codeBlockStyle: 'fenced',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a URL is from a priority domain
|
|
25
|
+
*/
|
|
26
|
+
isPriorityDomain(url) {
|
|
27
|
+
return SCRAPER_CONFIG.PRIORITY_DOMAINS.some(domain => url.includes(domain));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Scrape a webpage and return structured data
|
|
31
|
+
* Overrides the base class method with a specific implementation
|
|
32
|
+
*/
|
|
33
|
+
async scrape(url, extractor) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetchWithRetry(url, {
|
|
36
|
+
headers: {
|
|
37
|
+
'User-Agent': SCRAPER_CONFIG.USER_AGENT,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const html = await response.text();
|
|
41
|
+
if (extractor) {
|
|
42
|
+
const data = extractor(html);
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
data,
|
|
46
|
+
provider: 'scraper',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Default extraction: extract article content
|
|
50
|
+
const doc = new JSDOM(html, { url });
|
|
51
|
+
const reader = new Readability(doc.window.document);
|
|
52
|
+
const article = reader.parse();
|
|
53
|
+
if (!article) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: 'Could not parse article content',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let content = this.turndown.turndown(article.content || '');
|
|
60
|
+
// Truncate if too long
|
|
61
|
+
if (content.length > SCRAPER_CONFIG.MAX_CONTENT_LENGTH) {
|
|
62
|
+
content = content.substring(0, SCRAPER_CONFIG.MAX_CONTENT_LENGTH) + '\n...(truncated)';
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
data: `Title: ${article.title}\n\n${content}`,
|
|
67
|
+
provider: 'scraper',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: error instanceof Error ? error.message : String(error),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Public method to scrape and get markdown content
|
|
79
|
+
*/
|
|
80
|
+
async scrapeToMarkdown(url) {
|
|
81
|
+
return this.scrape(url);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract match data from a webpage
|
|
85
|
+
*/
|
|
86
|
+
async getMatch(matchId) {
|
|
87
|
+
// For scraper, matchId should be a URL
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: 'Direct match scraping not implemented. Use URL-based scraping.',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get live matches from a scoring website
|
|
95
|
+
*/
|
|
96
|
+
async getLiveMatches(leagueId) {
|
|
97
|
+
// This would require scraping live score websites
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
error: 'Live score scraping not implemented. Use API providers.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get standings from a webpage
|
|
105
|
+
*/
|
|
106
|
+
async getStandings(leagueId) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: 'Standings scraping not implemented. Use API providers.',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get team information from a webpage
|
|
114
|
+
*/
|
|
115
|
+
async getTeam(teamId) {
|
|
116
|
+
// teamId should be a URL
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'Team scraping not implemented. Use URL-based scraping.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get player information from a webpage
|
|
124
|
+
*/
|
|
125
|
+
async getPlayer(playerId) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: 'Player scraping not implemented. Use API providers.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Search for teams via web search
|
|
133
|
+
*/
|
|
134
|
+
async searchTeams(query) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: 'Team search via scraping not implemented.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Search for players via web search
|
|
142
|
+
*/
|
|
143
|
+
async searchPlayers(query) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: 'Player search via scraping not implemented.',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Scrape a URL with timeout protection
|
|
151
|
+
*/
|
|
152
|
+
async scrapeWithTimeout(url, timeout = SCRAPER_CONFIG.TIMEOUT) {
|
|
153
|
+
const scrapePromise = this.scrapeToMarkdown(url);
|
|
154
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Scrape timeout')), timeout));
|
|
155
|
+
try {
|
|
156
|
+
return await Promise.race([scrapePromise, timeoutPromise]);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: error instanceof Error ? error.message : String(error),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Scrape match preview/article content
|
|
168
|
+
*/
|
|
169
|
+
export async function scrapeMatchContent(url) {
|
|
170
|
+
const scraper = new ScraperProvider();
|
|
171
|
+
return scraper.scrapeToMarkdown(url);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Find best URL for match data from search results
|
|
175
|
+
*/
|
|
176
|
+
export function findBestMatchUrl(urls) {
|
|
177
|
+
const priorityDomains = SCRAPER_CONFIG.PRIORITY_DOMAINS;
|
|
178
|
+
// Find first URL from priority domain
|
|
179
|
+
for (const url of urls) {
|
|
180
|
+
if (priorityDomains.some(domain => url.includes(domain))) {
|
|
181
|
+
return url;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Return first URL if no priority match
|
|
185
|
+
return urls[0] || null;
|
|
186
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPORTS MODULE SEARCH PROVIDER
|
|
3
|
+
* Abstraction layer for web search providers (Brave, Exa, Google)
|
|
4
|
+
*/
|
|
5
|
+
import { SearchQuery, QueryType } from '../core/types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Search result
|
|
8
|
+
*/
|
|
9
|
+
export interface SearchResult {
|
|
10
|
+
title: string;
|
|
11
|
+
url: string;
|
|
12
|
+
snippet: string;
|
|
13
|
+
publishedDate?: string;
|
|
14
|
+
score?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Unified Search Provider with fallback
|
|
18
|
+
*/
|
|
19
|
+
export declare class SearchProvider {
|
|
20
|
+
private providers;
|
|
21
|
+
constructor();
|
|
22
|
+
/**
|
|
23
|
+
* Get available providers
|
|
24
|
+
*/
|
|
25
|
+
getAvailableProviders(): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Search with automatic fallback
|
|
28
|
+
*/
|
|
29
|
+
search(query: string, preferredProvider?: string, count?: number): Promise<SearchResult[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Perform multiple searches in parallel
|
|
32
|
+
*/
|
|
33
|
+
searchMultiple(queries: SearchQuery[], count?: number): Promise<Map<QueryType, SearchResult[]>>;
|
|
34
|
+
/**
|
|
35
|
+
* Build football match search queries
|
|
36
|
+
*/
|
|
37
|
+
static buildMatchQueries(homeTeam: string, awayTeam: string, league?: string, context?: string): SearchQuery[];
|
|
38
|
+
/**
|
|
39
|
+
* Extract URLs from search results
|
|
40
|
+
*/
|
|
41
|
+
static extractURLs(results: SearchResult[]): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Format search results as markdown
|
|
44
|
+
*/
|
|
45
|
+
static formatAsMarkdown(results: SearchResult[], title: string): string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get or create the global search provider
|
|
49
|
+
*/
|
|
50
|
+
export declare function getSearchProvider(): SearchProvider;
|
|
51
|
+
/**
|
|
52
|
+
* Reset the global search provider
|
|
53
|
+
*/
|
|
54
|
+
export declare function resetSearchProvider(): void;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPORTS MODULE SEARCH PROVIDER
|
|
3
|
+
* Abstraction layer for web search providers (Brave, Exa, Google)
|
|
4
|
+
*/
|
|
5
|
+
import { QUERY_TYPES } from '../core/constants.js';
|
|
6
|
+
import { fetchWithRetry } from '../../../utils.js';
|
|
7
|
+
import { logger } from '../../../utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Brave Search Provider
|
|
10
|
+
*/
|
|
11
|
+
class BraveSearchProvider {
|
|
12
|
+
name = 'brave';
|
|
13
|
+
isAvailable() {
|
|
14
|
+
return !!process.env.BRAVE_API_KEY;
|
|
15
|
+
}
|
|
16
|
+
async search(query, count = 5) {
|
|
17
|
+
const response = await fetchWithRetry(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`, {
|
|
18
|
+
headers: {
|
|
19
|
+
'X-Subscription-Token': process.env.BRAVE_API_KEY,
|
|
20
|
+
'Accept': 'application/json',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Brave API error: ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
const results = data.web?.results || [];
|
|
28
|
+
return results.map((r) => ({
|
|
29
|
+
title: r.title,
|
|
30
|
+
url: r.url,
|
|
31
|
+
snippet: r.description,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Exa Search Provider
|
|
37
|
+
*/
|
|
38
|
+
class ExaSearchProvider {
|
|
39
|
+
name = 'exa';
|
|
40
|
+
isAvailable() {
|
|
41
|
+
return !!process.env.EXA_API_KEY;
|
|
42
|
+
}
|
|
43
|
+
async search(query, count = 5) {
|
|
44
|
+
const response = await fetchWithRetry('https://api.exa.ai/search', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'x-api-key': process.env.EXA_API_KEY,
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({ query, numResults: count }),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`Exa API error: ${response.status}`);
|
|
54
|
+
}
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
const results = data.results || [];
|
|
57
|
+
return results.map((r) => ({
|
|
58
|
+
title: r.title,
|
|
59
|
+
url: r.url,
|
|
60
|
+
snippet: r.text || r.title,
|
|
61
|
+
publishedDate: r.publishedDate,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Google Custom Search Provider
|
|
67
|
+
*/
|
|
68
|
+
class GoogleSearchProvider {
|
|
69
|
+
name = 'google';
|
|
70
|
+
isAvailable() {
|
|
71
|
+
return !!(process.env.GOOGLE_SEARCH_API_KEY && process.env.GOOGLE_SEARCH_CX);
|
|
72
|
+
}
|
|
73
|
+
async search(query, count = 5) {
|
|
74
|
+
const response = await fetchWithRetry(`https://www.googleapis.com/customsearch/v1?key=${process.env.GOOGLE_SEARCH_API_KEY}&cx=${process.env.GOOGLE_SEARCH_CX}&q=${encodeURIComponent(query)}&num=${count}`);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Google API error: ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
const items = data.items || [];
|
|
80
|
+
return items.map((item) => ({
|
|
81
|
+
title: item.title,
|
|
82
|
+
url: item.link,
|
|
83
|
+
snippet: item.snippet,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Unified Search Provider with fallback
|
|
89
|
+
*/
|
|
90
|
+
export class SearchProvider {
|
|
91
|
+
providers = [];
|
|
92
|
+
constructor() {
|
|
93
|
+
// Initialize available providers
|
|
94
|
+
const brave = new BraveSearchProvider();
|
|
95
|
+
if (brave.isAvailable()) {
|
|
96
|
+
this.providers.push(brave);
|
|
97
|
+
logger.info('Brave search provider initialized');
|
|
98
|
+
}
|
|
99
|
+
const exa = new ExaSearchProvider();
|
|
100
|
+
if (exa.isAvailable()) {
|
|
101
|
+
this.providers.push(exa);
|
|
102
|
+
logger.info('Exa search provider initialized');
|
|
103
|
+
}
|
|
104
|
+
const google = new GoogleSearchProvider();
|
|
105
|
+
if (google.isAvailable()) {
|
|
106
|
+
this.providers.push(google);
|
|
107
|
+
logger.info('Google search provider initialized');
|
|
108
|
+
}
|
|
109
|
+
if (this.providers.length === 0) {
|
|
110
|
+
logger.warn('No search providers available. Set BRAVE_API_KEY, EXA_API_KEY, or GOOGLE_SEARCH_API_KEY.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get available providers
|
|
115
|
+
*/
|
|
116
|
+
getAvailableProviders() {
|
|
117
|
+
return this.providers.map(p => p.name);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Search with automatic fallback
|
|
121
|
+
*/
|
|
122
|
+
async search(query, preferredProvider, count = 5) {
|
|
123
|
+
// Sort providers: preferred first, then others
|
|
124
|
+
let providers = this.providers;
|
|
125
|
+
if (preferredProvider) {
|
|
126
|
+
const preferred = this.providers.find(p => p.name === preferredProvider);
|
|
127
|
+
providers = preferred
|
|
128
|
+
? [preferred, ...this.providers.filter(p => p.name !== preferredProvider)]
|
|
129
|
+
: this.providers;
|
|
130
|
+
}
|
|
131
|
+
const errors = [];
|
|
132
|
+
for (const provider of providers) {
|
|
133
|
+
try {
|
|
134
|
+
const results = await provider.search(query, count);
|
|
135
|
+
logger.debug(`${provider.name} search returned ${results.length} results`);
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
140
|
+
errors.push(`${provider.name}: ${errorMsg}`);
|
|
141
|
+
logger.warn(`${provider.name} search failed: ${errorMsg}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`All search providers failed:\n${errors.join('\n')}`);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Perform multiple searches in parallel
|
|
148
|
+
*/
|
|
149
|
+
async searchMultiple(queries, count = 4) {
|
|
150
|
+
const results = new Map();
|
|
151
|
+
// Sort by priority (higher first)
|
|
152
|
+
const sortedQueries = [...queries].sort((a, b) => b.priority - a.priority);
|
|
153
|
+
// Search in parallel
|
|
154
|
+
const searchPromises = sortedQueries.map(async (query) => {
|
|
155
|
+
try {
|
|
156
|
+
const searchResults = await this.search(query.query, undefined, count);
|
|
157
|
+
return { type: query.type, results: searchResults };
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger.error(`Search failed for ${query.type}: ${error}`);
|
|
161
|
+
return { type: query.type, results: [] };
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
const allSearchResults = await Promise.all(searchPromises);
|
|
165
|
+
for (const { type, results: searchResults } of allSearchResults) {
|
|
166
|
+
results.set(type, searchResults);
|
|
167
|
+
}
|
|
168
|
+
return results;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Build football match search queries
|
|
172
|
+
*/
|
|
173
|
+
static buildMatchQueries(homeTeam, awayTeam, league, context) {
|
|
174
|
+
const leagueStr = league ? `${league} ` : '';
|
|
175
|
+
const baseQuery = `${leagueStr}${homeTeam} vs ${awayTeam}`;
|
|
176
|
+
const today = new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
|
177
|
+
const dateQuery = ` ${today}`;
|
|
178
|
+
return [
|
|
179
|
+
{ type: QUERY_TYPES.GENERAL, query: `${baseQuery} match prediction stats${dateQuery}`, priority: 10 },
|
|
180
|
+
{ type: QUERY_TYPES.H2H, query: `${baseQuery} head to head history`, priority: 8 },
|
|
181
|
+
{ type: QUERY_TYPES.FORM, query: `${homeTeam} ${awayTeam} recent form last 5 matches${dateQuery}`, priority: 7 },
|
|
182
|
+
{ type: 'news', query: `${baseQuery} team news injuries lineups${dateQuery}`, priority: 9 },
|
|
183
|
+
{ type: QUERY_TYPES.STATS, query: `${baseQuery} xG expected goals stats${dateQuery}`, priority: 6 },
|
|
184
|
+
{ type: QUERY_TYPES.ODDS, query: `${baseQuery} odds movement dropping trends${dateQuery}`, priority: 8 },
|
|
185
|
+
{ type: QUERY_TYPES.FATIGUE, query: `${homeTeam} ${awayTeam} days rest fixture congestion${dateQuery}`, priority: 5 },
|
|
186
|
+
{ type: QUERY_TYPES.SETPIECES, query: `${baseQuery} set pieces corners aerial duels${dateQuery}`, priority: 5 },
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Extract URLs from search results
|
|
191
|
+
*/
|
|
192
|
+
static extractURLs(results) {
|
|
193
|
+
return results.map(r => r.url);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Format search results as markdown
|
|
197
|
+
*/
|
|
198
|
+
static formatAsMarkdown(results, title) {
|
|
199
|
+
if (results.length === 0) {
|
|
200
|
+
return `### ${title}\nNo results found.\n`;
|
|
201
|
+
}
|
|
202
|
+
const items = results.map(r => `- [${r.title}](${r.url}): ${r.snippet}`).join('\n');
|
|
203
|
+
return `### ${title}\n${items}\n`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Singleton search provider instance
|
|
208
|
+
*/
|
|
209
|
+
let globalSearchProvider = null;
|
|
210
|
+
/**
|
|
211
|
+
* Get or create the global search provider
|
|
212
|
+
*/
|
|
213
|
+
export function getSearchProvider() {
|
|
214
|
+
if (!globalSearchProvider) {
|
|
215
|
+
globalSearchProvider = new SearchProvider();
|
|
216
|
+
}
|
|
217
|
+
return globalSearchProvider;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Reset the global search provider
|
|
221
|
+
*/
|
|
222
|
+
export function resetSearchProvider() {
|
|
223
|
+
globalSearchProvider = null;
|
|
224
|
+
}
|