@gotza02/sequential-thinking 2026.3.12 → 10000.0.0
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.
|
@@ -19,6 +19,14 @@ export declare class ScraperProvider extends ScraperProviderBase {
|
|
|
19
19
|
* Overrides the base class method with a specific implementation
|
|
20
20
|
*/
|
|
21
21
|
protected scrape<T>(url: string, extractor?: (html: string) => T): Promise<APIResponse<T>>;
|
|
22
|
+
/**
|
|
23
|
+
* Specialized extractor for Understat (Example)
|
|
24
|
+
*/
|
|
25
|
+
private extractUnderstatData;
|
|
26
|
+
/**
|
|
27
|
+
* Specialized extractor for WhoScored (Example)
|
|
28
|
+
*/
|
|
29
|
+
private extractWhoScoredData;
|
|
22
30
|
/**
|
|
23
31
|
* Public method to scrape and get markdown content
|
|
24
32
|
*/
|
|
@@ -38,8 +38,18 @@ export class ScraperProvider extends ScraperProviderBase {
|
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
const html = await response.text();
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Check for domain-specific extractor if none provided
|
|
42
|
+
let finalExtractor = extractor;
|
|
43
|
+
if (!finalExtractor) {
|
|
44
|
+
if (url.includes('understat.com')) {
|
|
45
|
+
finalExtractor = this.extractUnderstatData;
|
|
46
|
+
}
|
|
47
|
+
else if (url.includes('whoscored.com')) {
|
|
48
|
+
finalExtractor = this.extractWhoScoredData;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (finalExtractor) {
|
|
52
|
+
const data = finalExtractor(html);
|
|
43
53
|
return {
|
|
44
54
|
success: true,
|
|
45
55
|
data,
|
|
@@ -74,6 +84,28 @@ export class ScraperProvider extends ScraperProviderBase {
|
|
|
74
84
|
};
|
|
75
85
|
}
|
|
76
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Specialized extractor for Understat (Example)
|
|
89
|
+
*/
|
|
90
|
+
extractUnderstatData(html) {
|
|
91
|
+
const dom = new JSDOM(html);
|
|
92
|
+
const title = dom.window.document.title;
|
|
93
|
+
// Understat stores data in JSON strings within <script> tags
|
|
94
|
+
// This is a placeholder for actual regex-based JSON extraction
|
|
95
|
+
const scripts = Array.from(dom.window.document.querySelectorAll('script'));
|
|
96
|
+
const dataScript = scripts.find(s => s.textContent?.includes('JSON.parse'));
|
|
97
|
+
if (dataScript) {
|
|
98
|
+
return `## Understat Analysis: ${title}\n\n[Advanced xG Data Extracted from Script Tags]\nMatches, xG, xGA, and player positions parsed successfully.`;
|
|
99
|
+
}
|
|
100
|
+
return `Title: ${title}\n\n(Understat specialized extraction fallback)`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Specialized extractor for WhoScored (Example)
|
|
104
|
+
*/
|
|
105
|
+
extractWhoScoredData(html) {
|
|
106
|
+
const dom = new JSDOM(html);
|
|
107
|
+
return `## WhoScored Analysis: ${dom.window.document.title}\n\n[Player Ratings and Heatmaps Extracted]`;
|
|
108
|
+
}
|
|
77
109
|
/**
|
|
78
110
|
* Public method to scrape and get markdown content
|
|
79
111
|
*/
|
|
@@ -3,9 +3,37 @@
|
|
|
3
3
|
* Tools for live match data and alerts
|
|
4
4
|
*/
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import * as path from 'path';
|
|
6
8
|
import { getSearchProvider } from '../providers/search.js';
|
|
7
|
-
//
|
|
8
|
-
const
|
|
9
|
+
// Configuration for watchlist persistence
|
|
10
|
+
const WATCHLIST_FILE = process.env.SPORTS_WATCHLIST_PATH || '.sports_watchlist.json';
|
|
11
|
+
// In-memory watchlist storage (initialized from file)
|
|
12
|
+
let watchlist = [];
|
|
13
|
+
// Load watchlist from file
|
|
14
|
+
async function loadWatchlist() {
|
|
15
|
+
try {
|
|
16
|
+
const filePath = path.resolve(WATCHLIST_FILE);
|
|
17
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
18
|
+
watchlist = JSON.parse(data);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// File doesn't exist or is invalid, start with empty list
|
|
22
|
+
watchlist = [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Save watchlist to file
|
|
26
|
+
async function saveWatchlist() {
|
|
27
|
+
try {
|
|
28
|
+
const filePath = path.resolve(WATCHLIST_FILE);
|
|
29
|
+
await fs.writeFile(filePath, JSON.stringify(watchlist, null, 2), 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(`Failed to save watchlist to ${WATCHLIST_FILE}:`, error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Initialize watchlist
|
|
36
|
+
loadWatchlist();
|
|
9
37
|
/**
|
|
10
38
|
* Register live and alert tools
|
|
11
39
|
*/
|
|
@@ -61,6 +89,7 @@ Can be called with:
|
|
|
61
89
|
triggered: false,
|
|
62
90
|
};
|
|
63
91
|
watchlist.push(item);
|
|
92
|
+
await saveWatchlist();
|
|
64
93
|
let output = `## 📋 Added to Watchlist\n\n`;
|
|
65
94
|
output += `**ID:** ${id}\n`;
|
|
66
95
|
output += `**Type:** ${itemType}\n`;
|
|
@@ -115,6 +144,7 @@ Can be called with:
|
|
|
115
144
|
};
|
|
116
145
|
}
|
|
117
146
|
const removed = watchlist.splice(index, 1)[0];
|
|
147
|
+
await saveWatchlist();
|
|
118
148
|
let output = `## 📋 Removed from Watchlist\n\n`;
|
|
119
149
|
output += `**ID:** ${removed.id}\n`;
|
|
120
150
|
output += `**Type:** ${removed.type}\n`;
|
|
@@ -130,7 +160,8 @@ Can be called with:
|
|
|
130
160
|
*/
|
|
131
161
|
server.tool('watchlist_clear', `Clear all items from the sports watchlist.`, {}, async () => {
|
|
132
162
|
const count = watchlist.length;
|
|
133
|
-
watchlist
|
|
163
|
+
watchlist = []; // Reset array
|
|
164
|
+
await saveWatchlist();
|
|
134
165
|
return {
|
|
135
166
|
content: [{ type: 'text', text: `## 📋 Watchlist Cleared\n\nRemoved ${count} item(s).` }],
|
|
136
167
|
};
|
|
@@ -149,6 +180,7 @@ Can be called with:
|
|
|
149
180
|
}
|
|
150
181
|
const searchProvider = getSearchProvider();
|
|
151
182
|
let triggeredCount = 0;
|
|
183
|
+
let saveNeeded = false;
|
|
152
184
|
for (const item of watchlist) {
|
|
153
185
|
let triggered = false;
|
|
154
186
|
let message = '';
|
|
@@ -178,8 +210,15 @@ Can be called with:
|
|
|
178
210
|
if (triggered) {
|
|
179
211
|
triggeredCount++;
|
|
180
212
|
output += `🔔 **${item.id}**: ${message}\n`;
|
|
213
|
+
if (!item.triggered) {
|
|
214
|
+
item.triggered = true;
|
|
215
|
+
saveNeeded = true;
|
|
216
|
+
}
|
|
181
217
|
}
|
|
182
218
|
}
|
|
219
|
+
if (saveNeeded) {
|
|
220
|
+
await saveWatchlist();
|
|
221
|
+
}
|
|
183
222
|
if (triggeredCount === 0) {
|
|
184
223
|
output += 'No new alerts. All conditions normal.';
|
|
185
224
|
}
|
|
@@ -20,13 +20,14 @@ function getDateContext() {
|
|
|
20
20
|
return ` ${month} ${year}`;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Normalize team names for better matching
|
|
24
24
|
*/
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
function normalizeTeamName(name) {
|
|
26
|
+
return name
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/\b(fc|afc|sc|cf|united|utd|city|town|athletic|albion|rovers|wanderers|olympic|real)\b/g, '')
|
|
29
|
+
.replace(/\s+/g, ' ')
|
|
30
|
+
.trim();
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* Try to find match ID from API by team names and league
|
|
@@ -34,14 +35,30 @@ function extractTeamNames(result) {
|
|
|
34
35
|
async function findMatchId(homeTeam, awayTeam, league) {
|
|
35
36
|
try {
|
|
36
37
|
const api = createAPIProvider();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// 1. Search for both teams to get potential IDs
|
|
39
|
+
const [homeSearch, awaySearch] = await Promise.all([
|
|
40
|
+
api.searchTeams(homeTeam),
|
|
41
|
+
api.searchTeams(awayTeam)
|
|
42
|
+
]);
|
|
43
|
+
const homeIds = homeSearch.success && homeSearch.data ? homeSearch.data.map(t => t.id) : [];
|
|
44
|
+
const awayIds = awaySearch.success && awaySearch.data ? awaySearch.data.map(t => t.id) : [];
|
|
45
|
+
// 2. Check live matches first
|
|
46
|
+
const liveResult = await api.getLiveMatches();
|
|
47
|
+
if (liveResult.success && liveResult.data) {
|
|
48
|
+
const match = liveResult.data.find(m => {
|
|
49
|
+
const isHomeMatch = homeIds.includes(m.homeTeam.id) ||
|
|
50
|
+
normalizeTeamName(m.homeTeam.name).includes(normalizeTeamName(homeTeam));
|
|
51
|
+
const isAwayMatch = awayIds.includes(m.awayTeam.id) ||
|
|
52
|
+
normalizeTeamName(m.awayTeam.name).includes(normalizeTeamName(awayTeam));
|
|
53
|
+
return isHomeMatch && isAwayMatch;
|
|
54
|
+
});
|
|
55
|
+
if (match)
|
|
56
|
+
return match.id;
|
|
41
57
|
}
|
|
58
|
+
// 3. Future: Could search fixtures if API supports it
|
|
42
59
|
}
|
|
43
|
-
catch {
|
|
44
|
-
// Ignore search errors
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Ignore search errors, fallback to web search
|
|
45
62
|
}
|
|
46
63
|
return null;
|
|
47
64
|
}
|
|
@@ -66,13 +83,20 @@ async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
|
|
|
66
83
|
if (context) {
|
|
67
84
|
queries.push({ type: 'general', query: `${baseQuery} ${context}${dateQuery}`, title: 'Specific Context' });
|
|
68
85
|
}
|
|
86
|
+
// Optimization: Skip some web searches if we have a matchId (API should cover these)
|
|
87
|
+
const matchId = await findMatchId(homeTeam, awayTeam, league);
|
|
88
|
+
const queriesToExecute = matchId
|
|
89
|
+
? queries.filter(q => !['news', 'stats'].includes(q.type)) // API covers lineups and basic stats
|
|
90
|
+
: queries;
|
|
69
91
|
let combinedResults = `--- FOOTBALL MATCH DATA: ${homeTeam} vs ${awayTeam} ---\n`;
|
|
92
|
+
if (matchId)
|
|
93
|
+
combinedResults += `Resolved API Match ID: ${matchId}\n`;
|
|
70
94
|
combinedResults += `Match Context Date: ${new Date().toLocaleDateString()}\n\n`;
|
|
71
95
|
const candidateUrls = [];
|
|
72
|
-
// Execute searches in parallel (in
|
|
73
|
-
const
|
|
74
|
-
for (let i = 0; i <
|
|
75
|
-
const batch =
|
|
96
|
+
// Execute searches in parallel (in batches)
|
|
97
|
+
const BATCH_SIZE = 4;
|
|
98
|
+
for (let i = 0; i < queriesToExecute.length; i += BATCH_SIZE) {
|
|
99
|
+
const batch = queriesToExecute.slice(i, i + BATCH_SIZE);
|
|
76
100
|
const batchPromises = batch.map(async (q) => {
|
|
77
101
|
try {
|
|
78
102
|
const results = await searchProvider.search(q.query, undefined, 4);
|