@gotza02/sequential-thinking 10000.0.1 → 10000.0.3

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 CHANGED
@@ -17,6 +17,12 @@ Forces the AI to think before acting using a strict block-based flow:
17
17
  - **Auto-Backup:** Automatically creates `.bak` backups before modifying any file via `deep_code_edit`.
18
18
  - **Destructive Action Protection:** Prevents accidental data loss during code refactoring.
19
19
 
20
+ ### ⚽ Sports Intelligence (Upgraded)
21
+ - **Dual-Source Deep Dive:** Simultaneously scrapes news (BBC, Sky) and stats (Understat, FBref) for comprehensive coverage.
22
+ - **Smart xG Extraction:** "Smart Extractor" technology specifically for Understat to get accurate Expected Goals data.
23
+ - **Handicap Odds:** Dedicated search protocols for Asian Handicap and betting market analysis.
24
+ - **Fallback Protection:** Intelligent warning system when API keys are missing.
25
+
20
26
  ### 🌐 Web Search Integration
21
27
  - Built-in support for **Exa**, **Brave**, and **Google Search**.
22
28
  - Allows the AI to "pause and research" during the thinking process.
@@ -31,7 +31,8 @@ export declare const CACHE_CONFIG: {
31
31
  readonly MAX_SIZE: 1000;
32
32
  };
33
33
  export declare const SCRAPER_CONFIG: {
34
- readonly PRIORITY_DOMAINS: readonly ["whoscored.com", "sofascore.com", "flashscore.com", "understat.com", "fbref.com", "bbc.co.uk/sport", "skysports.com", "sportsmole.co.uk", "goal.com", "thesquareball.net"];
34
+ readonly PRIORITY_DOMAINS: readonly ["understat.com", "bbc.co.uk/sport", "sportsmole.co.uk", "skysports.com", "goal.com", "thesquareball.net", "fbref.com", "whoscored.com", "sofascore.com", "flashscore.com"];
35
+ readonly NEWS_DOMAINS: readonly ["bbc.co.uk/sport", "skysports.com", "sportsmole.co.uk", "standard.co.uk/sport", "goal.com", "espn.co.uk/football", "talksport.com", "mirror.co.uk/sport", "dailymail.co.uk/sport"];
35
36
  readonly TIMEOUT: 15000;
36
37
  readonly USER_AGENT: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
37
38
  readonly MAX_CONTENT_LENGTH: 50000;
@@ -44,16 +44,28 @@ export const CACHE_CONFIG = {
44
44
  export const SCRAPER_CONFIG = {
45
45
  // Priority domains for sports data
46
46
  PRIORITY_DOMAINS: [
47
- 'whoscored.com',
48
- 'sofascore.com',
49
- 'flashscore.com',
50
- 'understat.com',
51
- 'fbref.com',
47
+ 'understat.com', // High accuracy (Smart Extractor)
48
+ 'bbc.co.uk/sport', // Text-heavy, reliable
49
+ 'sportsmole.co.uk', // Text-heavy, reliable
50
+ 'skysports.com', // Good text/stats mix
51
+ 'goal.com', // Text-heavy
52
+ 'thesquareball.net', // Text-heavy
53
+ 'fbref.com', // Structured data
54
+ 'whoscored.com', // Hard to scrape (JS heavy)
55
+ 'sofascore.com', // Hard to scrape (JS heavy)
56
+ 'flashscore.com', // Hard to scrape (JS heavy)
57
+ ],
58
+ // News-focused domains (for lineups, injuries)
59
+ NEWS_DOMAINS: [
52
60
  'bbc.co.uk/sport',
53
61
  'skysports.com',
54
62
  'sportsmole.co.uk',
63
+ 'standard.co.uk/sport',
55
64
  'goal.com',
56
- 'thesquareball.net'
65
+ 'espn.co.uk/football',
66
+ 'talksport.com',
67
+ 'mirror.co.uk/sport',
68
+ 'dailymail.co.uk/sport'
57
69
  ],
58
70
  // Scraping timeout in milliseconds
59
71
  TIMEOUT: 15000,
@@ -20,7 +20,8 @@ export declare class ScraperProvider extends ScraperProviderBase {
20
20
  */
21
21
  protected scrape<T>(url: string, extractor?: (html: string) => T): Promise<APIResponse<T>>;
22
22
  /**
23
- * Specialized extractor for Understat (Example)
23
+ * Specialized extractor for Understat
24
+ * Extracts xG, shots, and other advanced metrics from the JSON data embedded in script tags.
24
25
  */
25
26
  private extractUnderstatData;
26
27
  /**
@@ -72,3 +73,7 @@ export declare function scrapeMatchContent(url: string): Promise<APIResponse<str
72
73
  * Find best URL for match data from search results
73
74
  */
74
75
  export declare function findBestMatchUrl(urls: string[]): string | null;
76
+ /**
77
+ * Find best URL for news/lineups from search results
78
+ */
79
+ export declare function findBestNewsUrl(urls: string[]): string | null;
@@ -85,19 +85,62 @@ export class ScraperProvider extends ScraperProviderBase {
85
85
  }
86
86
  }
87
87
  /**
88
- * Specialized extractor for Understat (Example)
88
+ * Specialized extractor for Understat
89
+ * Extracts xG, shots, and other advanced metrics from the JSON data embedded in script tags.
89
90
  */
90
91
  extractUnderstatData(html) {
91
92
  const dom = new JSDOM(html);
92
93
  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.`;
94
+ let output = `## Understat Analysis: ${title}\n\n`;
95
+ // Helper to extract JSON from script variables
96
+ const extractJson = (varName) => {
97
+ const scripts = Array.from(dom.window.document.querySelectorAll('script'));
98
+ for (const script of scripts) {
99
+ const content = script.textContent || '';
100
+ const regex = new RegExp(`var ${varName}\\s*=\\s*JSON\\.parse\\('([^']+)'\\)`);
101
+ const match = content.match(regex);
102
+ if (match && match[1]) {
103
+ try {
104
+ // Decode hex sequences if any (Understat sometimes uses hex encoding)
105
+ const jsonString = match[1].replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
106
+ return JSON.parse(jsonString);
107
+ }
108
+ catch (e) {
109
+ return null;
110
+ }
111
+ }
112
+ }
113
+ return null;
114
+ };
115
+ const matchData = extractJson('match_info');
116
+ const rosterData = extractJson('rostersData');
117
+ if (matchData) {
118
+ output += `### Match Overview\n`;
119
+ output += `**Team:** ${matchData.h_team} vs ${matchData.a_team}\n`;
120
+ output += `**Score:** ${matchData.h_goals} - ${matchData.a_goals}\n`;
121
+ output += `**xG:** ${matchData.h_xg} - ${matchData.a_xg}\n`;
122
+ output += `**Probabilities:** Home Win: ${matchData.h_win}, Draw: ${matchData.draw}, Away Win: ${matchData.a_win}\n\n`;
99
123
  }
100
- return `Title: ${title}\n\n(Understat specialized extraction fallback)`;
124
+ if (rosterData) {
125
+ output += `### Key Player Stats (xG > 0.1)\n`;
126
+ const processPlayers = (players, teamName) => {
127
+ output += `**${teamName}:**\n`;
128
+ Object.values(players).forEach((p) => {
129
+ if (parseFloat(p.xG) > 0.1) {
130
+ output += `- ${p.player}: xG ${p.xG}, xA ${p.xA}, Shots: ${p.shots}\n`;
131
+ }
132
+ });
133
+ output += '\n';
134
+ };
135
+ if (rosterData.h)
136
+ processPlayers(rosterData.h, matchData?.h_team || 'Home');
137
+ if (rosterData.a)
138
+ processPlayers(rosterData.a, matchData?.a_team || 'Away');
139
+ }
140
+ if (!matchData && !rosterData) {
141
+ return `Title: ${title}\n\n(Understat extraction failed - JSON data not found)`;
142
+ }
143
+ return output;
101
144
  }
102
145
  /**
103
146
  * Specialized extractor for WhoScored (Example)
@@ -216,3 +259,15 @@ export function findBestMatchUrl(urls) {
216
259
  // Return first URL if no priority match
217
260
  return urls[0] || null;
218
261
  }
262
+ /**
263
+ * Find best URL for news/lineups from search results
264
+ */
265
+ export function findBestNewsUrl(urls) {
266
+ const newsDomains = SCRAPER_CONFIG.NEWS_DOMAINS;
267
+ for (const url of urls) {
268
+ if (newsDomains.some(domain => url.includes(domain))) {
269
+ return url;
270
+ }
271
+ }
272
+ return null;
273
+ }
@@ -3,4 +3,8 @@
3
3
  * Tools for match analysis and live scores
4
4
  */
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ /**
7
+ * Perform comprehensive match analysis using web search
8
+ */
9
+ export declare function performMatchAnalysis(homeTeam: string, awayTeam: string, league?: string, context?: string): Promise<string>;
6
10
  export declare function registerMatchTools(server: McpServer): void;
@@ -5,7 +5,7 @@
5
5
  import { z } from 'zod';
6
6
  import { createAPIProvider } from '../providers/api.js';
7
7
  import { getSearchProvider } from '../providers/search.js';
8
- import { scrapeMatchContent, findBestMatchUrl } from '../providers/scraper.js';
8
+ import { scrapeMatchContent, findBestMatchUrl, findBestNewsUrl } from '../providers/scraper.js';
9
9
  import { getGlobalCache, CacheService } from '../core/cache.js';
10
10
  import { formatMatchesTable, formatScore, formatMatchStatus } from '../utils/formatter.js';
11
11
  import { CACHE_CONFIG, LEAGUES } from '../core/constants.js';
@@ -65,7 +65,7 @@ async function findMatchId(homeTeam, awayTeam, league) {
65
65
  /**
66
66
  * Perform comprehensive match analysis using web search
67
67
  */
68
- async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
68
+ export async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
69
69
  const leagueStr = league ? `${league} ` : '';
70
70
  const baseQuery = `${leagueStr}${homeTeam} vs ${awayTeam}`;
71
71
  const dateQuery = getDateContext();
@@ -76,7 +76,7 @@ async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
76
76
  { type: 'form', query: `${homeTeam} ${awayTeam} recent form last 5 matches${dateQuery}`, title: 'Recent Form' },
77
77
  { type: 'news', query: `${baseQuery} team news injuries lineups${dateQuery}`, title: 'Team News & Lineups' },
78
78
  { type: 'stats', query: `${baseQuery} xG expected goals stats${dateQuery}`, title: 'Advanced Metrics' },
79
- { type: 'odds', query: `${baseQuery} odds movement dropping odds${dateQuery}`, title: 'Market & Odds' },
79
+ { type: 'odds', query: `${baseQuery} Asian Handicap odds prediction today${dateQuery}`, title: 'Latest Handicap Odds' },
80
80
  { type: 'fatigue', query: `${homeTeam} ${awayTeam} days rest fixture congestion${dateQuery}`, title: 'Fatigue & Schedule' },
81
81
  { type: 'setpieces', query: `${baseQuery} set pieces corners aerial duels${dateQuery}`, title: 'Set Pieces' },
82
82
  ];
@@ -111,24 +111,45 @@ async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
111
111
  const batchResults = await Promise.all(batchPromises);
112
112
  combinedResults += batchResults.join('\n');
113
113
  }
114
- // Deep dive: Scrape the best URL
114
+ // Deep dive: Scrape the best URLs (Dual-Source Strategy)
115
115
  if (candidateUrls.length > 0) {
116
- const bestUrl = findBestMatchUrl(candidateUrls);
117
- if (bestUrl) {
118
- combinedResults += `\n--- DEEP DIVE ANALYSIS (${bestUrl}) ---\n`;
116
+ const bestStatsUrl = findBestMatchUrl(candidateUrls);
117
+ const bestNewsUrl = findBestNewsUrl(candidateUrls);
118
+ // 1. Scrape Stats Source (Priority)
119
+ if (bestStatsUrl) {
120
+ combinedResults += `\n--- DEEP DIVE ANALYSIS: STATS & DATA (${bestStatsUrl}) ---\n`;
119
121
  try {
120
- const scrapedContent = await scrapeMatchContent(bestUrl);
122
+ const scrapedContent = await scrapeMatchContent(bestStatsUrl);
121
123
  if (scrapedContent.success) {
122
124
  combinedResults += scrapedContent.data || '';
123
125
  }
124
126
  else {
125
- combinedResults += `(Deep dive failed: ${scrapedContent.error})`;
127
+ combinedResults += `(Stats scrape failed: ${scrapedContent.error})`;
126
128
  }
127
129
  }
128
130
  catch (scrapeError) {
129
- combinedResults += `(Deep dive failed: ${scrapeError instanceof Error ? scrapeError.message : String(scrapeError)})`;
131
+ combinedResults += `(Stats scrape failed: ${scrapeError instanceof Error ? scrapeError.message : String(scrapeError)})`;
130
132
  }
131
- combinedResults += `\n--- END DEEP DIVE ---\n`;
133
+ combinedResults += `\n--- END STATS ---\n`;
134
+ }
135
+ // 2. Scrape News Source (if different from stats)
136
+ if (bestNewsUrl && bestNewsUrl !== bestStatsUrl) {
137
+ combinedResults += `\n--- DEEP DIVE ANALYSIS: NEWS & LINEUPS (${bestNewsUrl}) ---\n`;
138
+ try {
139
+ const scrapedContent = await scrapeMatchContent(bestNewsUrl);
140
+ if (scrapedContent.success) {
141
+ // Truncate news to avoid blowing up context, keep first 3000 chars
142
+ const newsContent = scrapedContent.data?.substring(0, 3000) || '';
143
+ combinedResults += newsContent + (scrapedContent.data && scrapedContent.data.length > 3000 ? '\n...(truncated)' : '');
144
+ }
145
+ else {
146
+ combinedResults += `(News scrape failed: ${scrapedContent.error})`;
147
+ }
148
+ }
149
+ catch (scrapeError) {
150
+ combinedResults += `(News scrape failed: ${scrapeError instanceof Error ? scrapeError.message : String(scrapeError)})`;
151
+ }
152
+ combinedResults += `\n--- END NEWS ---\n`;
132
153
  }
133
154
  }
134
155
  combinedResults += `\n--- END DATA ---\n\n`;
@@ -161,19 +182,16 @@ Market Intelligence, Referee stats, Weather, Fatigue analysis, Set Pieces.`, {
161
182
  }, async ({ homeTeam, awayTeam, league, context, useApi }) => {
162
183
  const errors = [];
163
184
  let analysisResult = null;
185
+ // Check API availability explicitly
186
+ const api = createAPIProvider();
187
+ const apiAvailable = api.getStatus().available;
188
+ const forcedFallback = useApi && !apiAvailable;
164
189
  // Try API providers first if requested
165
- if (useApi) {
190
+ if (useApi && apiAvailable) {
166
191
  try {
167
- const api = createAPIProvider();
168
- const apiStatus = api.getStatus();
169
- if (apiStatus.available) {
170
- // Could try to fetch match data from API here
171
- // For now, we'll use the search-based analysis as fallback
172
- analysisResult = await performMatchAnalysis(homeTeam, awayTeam, league, context);
173
- }
174
- else {
175
- errors.push('API providers not configured');
176
- }
192
+ // Could try to fetch match data from API here
193
+ // For now, we'll use the search-based analysis as fallback
194
+ analysisResult = await performMatchAnalysis(homeTeam, awayTeam, league, context);
177
195
  }
178
196
  catch (error) {
179
197
  errors.push(`API error: ${error instanceof Error ? error.message : String(error)}`);
@@ -183,6 +201,11 @@ Market Intelligence, Referee stats, Weather, Fatigue analysis, Set Pieces.`, {
183
201
  if (!analysisResult) {
184
202
  analysisResult = await performMatchAnalysis(homeTeam, awayTeam, league, context);
185
203
  }
204
+ // Add warning if running in forced fallback mode
205
+ if (forcedFallback && analysisResult) {
206
+ const warning = "⚠️ **WARNING: Fallback Mode Active**\nNo API keys configured. Using web search and scraping (accuracy may vary).\n\n";
207
+ analysisResult = warning + analysisResult;
208
+ }
186
209
  return {
187
210
  content: [{ type: 'text', text: analysisResult }],
188
211
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { performMatchAnalysis } from './match.js';
3
+ // Mock dependencies
4
+ const mockSearchProvider = {
5
+ search: vi.fn(),
6
+ };
7
+ vi.mock('../providers/search.js', () => ({
8
+ getSearchProvider: () => mockSearchProvider,
9
+ SearchProvider: class {
10
+ },
11
+ }));
12
+ vi.mock('../providers/scraper.js', () => ({
13
+ scrapeMatchContent: vi.fn(),
14
+ findBestMatchUrl: vi.fn(),
15
+ findBestNewsUrl: vi.fn(),
16
+ }));
17
+ vi.mock('../providers/api.js', () => ({
18
+ createAPIProvider: () => ({
19
+ searchTeams: vi.fn().mockResolvedValue({ success: false }),
20
+ getLiveMatches: vi.fn().mockResolvedValue({ success: false }),
21
+ getStatus: () => ({ available: false })
22
+ })
23
+ }));
24
+ // Import mocked modules to setup return values
25
+ import { scrapeMatchContent, findBestMatchUrl, findBestNewsUrl } from '../providers/scraper.js';
26
+ describe('Match Analysis Tool', () => {
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ // Default mock behaviors
30
+ mockSearchProvider.search.mockResolvedValue([
31
+ { title: 'Result 1', url: 'https://example.com/1', snippet: 'Snippet 1' }
32
+ ]);
33
+ scrapeMatchContent.mockResolvedValue({ success: true, data: 'Scraped Content' });
34
+ findBestMatchUrl.mockReturnValue(null);
35
+ findBestNewsUrl.mockReturnValue(null);
36
+ });
37
+ it('should perform basic analysis with search results', async () => {
38
+ const result = await performMatchAnalysis('Arsenal', 'Chelsea');
39
+ expect(mockSearchProvider.search).toHaveBeenCalled();
40
+ expect(result).toContain('Arsenal vs Chelsea');
41
+ expect(result).toContain('Result 1');
42
+ expect(result).toContain('INSTRUCTIONS: Act as a World-Class Football Analysis Panel');
43
+ });
44
+ it('should scrape stats URL when found', async () => {
45
+ findBestMatchUrl.mockReturnValue('https://understat.com/match/123');
46
+ const result = await performMatchAnalysis('Arsenal', 'Chelsea');
47
+ expect(findBestMatchUrl).toHaveBeenCalled();
48
+ expect(scrapeMatchContent).toHaveBeenCalledWith('https://understat.com/match/123');
49
+ expect(result).toContain('DEEP DIVE ANALYSIS: STATS & DATA');
50
+ expect(result).toContain('Scraped Content');
51
+ });
52
+ it('should perform dual-source scraping (Stats + News)', async () => {
53
+ // Setup distinct URLs for stats and news
54
+ findBestMatchUrl.mockReturnValue('https://understat.com/match/123');
55
+ findBestNewsUrl.mockReturnValue('https://bbc.com/sport/football/456');
56
+ // Mock scraping to return different content based on URL
57
+ scrapeMatchContent.mockImplementation(async (url) => {
58
+ if (url.includes('understat'))
59
+ return { success: true, data: 'STATS_DATA_HERE' };
60
+ if (url.includes('bbc'))
61
+ return { success: true, data: 'NEWS_DATA_HERE' };
62
+ return { success: false };
63
+ });
64
+ const result = await performMatchAnalysis('Arsenal', 'Chelsea');
65
+ expect(scrapeMatchContent).toHaveBeenCalledTimes(2);
66
+ expect(result).toContain('DEEP DIVE ANALYSIS: STATS & DATA');
67
+ expect(result).toContain('STATS_DATA_HERE');
68
+ expect(result).toContain('DEEP DIVE ANALYSIS: NEWS & LINEUPS');
69
+ expect(result).toContain('NEWS_DATA_HERE');
70
+ });
71
+ it('should not scrape news if it is the same as stats URL', async () => {
72
+ const url = 'https://example.com/match';
73
+ findBestMatchUrl.mockReturnValue(url);
74
+ findBestNewsUrl.mockReturnValue(url); // Same URL
75
+ await performMatchAnalysis('Arsenal', 'Chelsea');
76
+ expect(scrapeMatchContent).toHaveBeenCalledTimes(1); // Should only scrape once
77
+ });
78
+ it('should handle scraper failures gracefully', async () => {
79
+ findBestMatchUrl.mockReturnValue('https://bad-url.com');
80
+ scrapeMatchContent.mockResolvedValue({ success: false, error: 'Timeout' });
81
+ const result = await performMatchAnalysis('Arsenal', 'Chelsea');
82
+ expect(result).toContain('Stats scrape failed: Timeout');
83
+ // Should still contain the main analysis structure
84
+ expect(result).toContain('INSTRUCTIONS: Act as a World-Class Football Analysis Panel');
85
+ });
86
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "10000.0.1",
3
+ "version": "10000.0.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },