@gotza02/sequential-thinking 10000.1.2 → 10000.1.4

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.
Files changed (32) hide show
  1. package/README.md +39 -249
  2. package/dist/dashboard/server.d.ts +2 -45
  3. package/dist/dashboard/server.js +64 -250
  4. package/dist/index.js +4 -1
  5. package/dist/tools/sports/core/base.d.ts +31 -1
  6. package/dist/tools/sports/core/base.js +25 -0
  7. package/dist/tools/sports/core/cache.d.ts +8 -19
  8. package/dist/tools/sports/core/cache.js +38 -95
  9. package/dist/tools/sports/core/constants.d.ts +4 -63
  10. package/dist/tools/sports/core/constants.js +11 -86
  11. package/dist/tools/sports/core/types.d.ts +146 -34
  12. package/dist/tools/sports/providers/api.d.ts +12 -1
  13. package/dist/tools/sports/providers/api.js +170 -0
  14. package/dist/tools/sports/tools/match.d.ts +5 -0
  15. package/dist/tools/sports/tools/match.js +530 -5
  16. package/package.json +1 -1
  17. package/dist/tools/sports/core/alert-manager.d.ts +0 -96
  18. package/dist/tools/sports/core/alert-manager.js +0 -319
  19. package/dist/tools/sports/core/circuit-breaker.d.ts +0 -40
  20. package/dist/tools/sports/core/circuit-breaker.js +0 -99
  21. package/dist/tools/sports/core/data-quality.d.ts +0 -36
  22. package/dist/tools/sports/core/data-quality.js +0 -243
  23. package/dist/tools/sports/core/historical-analyzer.d.ts +0 -54
  24. package/dist/tools/sports/core/historical-analyzer.js +0 -261
  25. package/dist/tools/sports/core/index.d.ts +0 -13
  26. package/dist/tools/sports/core/index.js +0 -16
  27. package/dist/tools/sports/core/ml-prediction.d.ts +0 -76
  28. package/dist/tools/sports/core/ml-prediction.js +0 -260
  29. package/dist/tools/sports/core/realtime-manager.d.ts +0 -51
  30. package/dist/tools/sports/core/realtime-manager.js +0 -222
  31. package/dist/tools/sports/core/retry.d.ts +0 -29
  32. package/dist/tools/sports/core/retry.js +0 -77
@@ -9,6 +9,7 @@ import { scrapeMatchContent, findBestMatchUrl, findBestNewsUrl } from '../provid
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';
12
+ import { logger } from '../../../utils.js';
12
13
  // ============= Helper Functions =============
13
14
  /**
14
15
  * Get date context for search queries
@@ -62,6 +63,200 @@ async function findMatchId(homeTeam, awayTeam, league) {
62
63
  }
63
64
  return null;
64
65
  }
66
+ /**
67
+ * Fetch comprehensive API data for a match
68
+ */
69
+ async function fetchAPIMatchData(api, matchId, homeTeam, awayTeam) {
70
+ try {
71
+ // Fetch all data in parallel
72
+ const [matchResult, h2hResult, oddsResult] = await Promise.all([
73
+ api.getMatch(matchId),
74
+ api.getH2H(matchId),
75
+ api.getMatchOdds(matchId),
76
+ ]);
77
+ let homeForm;
78
+ let awayForm;
79
+ // Get team form if we have match data
80
+ if (matchResult.success && matchResult.data) {
81
+ const [homeFormResult, awayFormResult] = await Promise.all([
82
+ api.getTeamForm(matchResult.data.homeTeam.id, 5),
83
+ api.getTeamForm(matchResult.data.awayTeam.id, 5),
84
+ ]);
85
+ if (homeFormResult.success && homeFormResult.data) {
86
+ homeForm = homeFormResult.data;
87
+ }
88
+ if (awayFormResult.success && awayFormResult.data) {
89
+ awayForm = awayFormResult.data;
90
+ }
91
+ }
92
+ return {
93
+ match: matchResult.success ? matchResult.data : undefined,
94
+ h2h: h2hResult.success ? h2hResult.data : undefined,
95
+ homeForm,
96
+ awayForm,
97
+ odds: oddsResult.success ? oddsResult.data : undefined,
98
+ lastUpdated: new Date(),
99
+ };
100
+ }
101
+ catch (error) {
102
+ logger.error(`Error fetching API match data: ${error}`);
103
+ return null;
104
+ }
105
+ }
106
+ /**
107
+ * Assess what data coverage we have from API
108
+ */
109
+ function assessDataCoverage(apiData) {
110
+ if (!apiData) {
111
+ return {
112
+ score: 0,
113
+ factors: {
114
+ hasAPIData: false,
115
+ hasRecentData: false,
116
+ sampleSize: 'small',
117
+ dataAge: 999,
118
+ missingDataPoints: ['all'],
119
+ },
120
+ };
121
+ }
122
+ let score = 0;
123
+ const missingDataPoints = [];
124
+ // Base match data
125
+ if (apiData.match) {
126
+ score += 40;
127
+ }
128
+ else {
129
+ missingDataPoints.push('match');
130
+ }
131
+ // H2H data
132
+ if (apiData.h2h && apiData.h2h.totalMatches > 0) {
133
+ score += 20;
134
+ }
135
+ else {
136
+ missingDataPoints.push('h2h');
137
+ }
138
+ // Form data
139
+ if (apiData.homeForm && apiData.homeForm.length >= 3) {
140
+ score += 15;
141
+ }
142
+ else {
143
+ missingDataPoints.push('homeForm');
144
+ }
145
+ if (apiData.awayForm && apiData.awayForm.length >= 3) {
146
+ score += 15;
147
+ }
148
+ else {
149
+ missingDataPoints.push('awayForm');
150
+ }
151
+ // Odds data
152
+ if (apiData.odds) {
153
+ score += 10;
154
+ }
155
+ else {
156
+ missingDataPoints.push('odds');
157
+ }
158
+ // Calculate data age
159
+ const dataAge = apiData.lastUpdated
160
+ ? (Date.now() - apiData.lastUpdated.getTime()) / (1000 * 60 * 60)
161
+ : 999;
162
+ // Determine sample size
163
+ let sampleSize = 'small';
164
+ const totalMatches = (apiData.homeForm?.length || 0) + (apiData.awayForm?.length || 0);
165
+ if (totalMatches >= 10)
166
+ sampleSize = 'large';
167
+ else if (totalMatches >= 5)
168
+ sampleSize = 'medium';
169
+ return {
170
+ score: Math.min(100, score),
171
+ factors: {
172
+ hasAPIData: score > 0,
173
+ hasRecentData: dataAge < 24,
174
+ sampleSize,
175
+ dataAge,
176
+ missingDataPoints,
177
+ },
178
+ };
179
+ }
180
+ /**
181
+ * Optimize search queries based on API data coverage
182
+ */
183
+ function optimizeQueries(coverage, baseQueries) {
184
+ // Always search for these (API doesn't cover well)
185
+ const alwaysSearch = ['fatigue', 'setpieces', 'referee', 'weather'];
186
+ return baseQueries.filter(q => {
187
+ // Always include critical queries
188
+ if (alwaysSearch.includes(q.type))
189
+ return true;
190
+ // Skip queries that API already covers well
191
+ switch (q.type) {
192
+ case 'h2h':
193
+ // Skip H2H search if we have good H2H data
194
+ return !coverage.factors.hasAPIData || coverage.factors.missingDataPoints.includes('h2h');
195
+ case 'form':
196
+ // Skip form search if we have form data
197
+ return !coverage.factors.hasAPIData ||
198
+ (coverage.factors.missingDataPoints.includes('homeForm') ||
199
+ coverage.factors.missingDataPoints.includes('awayForm'));
200
+ case 'stats':
201
+ // Stats need web search for xG
202
+ return true;
203
+ case 'news':
204
+ // Always search for news/lineups
205
+ return true;
206
+ case 'odds':
207
+ // Skip odds search if we have API odds
208
+ return !coverage.factors.hasAPIData || coverage.factors.missingDataPoints.includes('odds');
209
+ default:
210
+ return true;
211
+ }
212
+ });
213
+ }
214
+ /**
215
+ * Calculate adaptive TTL based on data type and match timing
216
+ */
217
+ function calculateAdaptiveTTL(dataType, matchDate) {
218
+ const now = Date.now();
219
+ const hoursUntilMatch = matchDate
220
+ ? (matchDate.getTime() - now) / (1000 * 60 * 60)
221
+ : Infinity;
222
+ switch (dataType) {
223
+ case 'odds':
224
+ // Odds change frequently - short TTL
225
+ if (hoursUntilMatch < 1)
226
+ return 60 * 1000; // 1 minute
227
+ if (hoursUntilMatch < 24)
228
+ return 5 * 60 * 1000; // 5 minutes
229
+ return 15 * 60 * 1000; // 15 minutes
230
+ case 'lineups':
231
+ // Lineups announced ~1 hour before match
232
+ if (hoursUntilMatch < 2)
233
+ return 5 * 60 * 1000; // 5 minutes
234
+ return 60 * 60 * 1000; // 1 hour
235
+ case 'news':
236
+ // News changes throughout the day
237
+ if (hoursUntilMatch < 24)
238
+ return 15 * 60 * 1000; // 15 minutes
239
+ return 60 * 60 * 1000; // 1 hour
240
+ case 'stats':
241
+ case 'h2h':
242
+ // Historical stats don't change
243
+ return 24 * 60 * 60 * 1000; // 24 hours
244
+ case 'form':
245
+ // Form updates after each match
246
+ if (hoursUntilMatch < 0)
247
+ return 60 * 1000; // Live match - 1 minute
248
+ return 6 * 60 * 60 * 1000; // 6 hours
249
+ case 'match':
250
+ // Match data changes based on timing
251
+ if (hoursUntilMatch < 0)
252
+ return 60 * 1000; // Live - 1 minute
253
+ if (hoursUntilMatch < 1)
254
+ return 5 * 60 * 1000; // 5 minutes
255
+ return 30 * 60 * 1000; // 30 minutes
256
+ default:
257
+ return 30 * 60 * 1000; // 30 minutes default
258
+ }
259
+ }
65
260
  /**
66
261
  * Perform comprehensive match analysis using web search
67
262
  */
@@ -83,15 +278,73 @@ export async function performMatchAnalysis(homeTeam, awayTeam, league, context)
83
278
  if (context) {
84
279
  queries.push({ type: 'general', query: `${baseQuery} ${context}${dateQuery}`, title: 'Specific Context' });
85
280
  }
86
- // Optimization: Skip some web searches if we have a matchId (API should cover these)
281
+ // Try to get API data first
282
+ const api = createAPIProvider();
87
283
  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;
284
+ let apiData = null;
285
+ if (matchId) {
286
+ apiData = await fetchAPIMatchData(api, matchId, homeTeam, awayTeam);
287
+ }
288
+ // Assess data coverage and optimize queries
289
+ const coverage = assessDataCoverage(apiData);
290
+ const queriesToExecute = optimizeQueries(coverage, queries);
91
291
  let combinedResults = `--- FOOTBALL MATCH DATA: ${homeTeam} vs ${awayTeam} ---\n`;
92
292
  if (matchId)
93
293
  combinedResults += `Resolved API Match ID: ${matchId}\n`;
294
+ combinedResults += `Data Quality Score: ${coverage.score}/100\n`;
94
295
  combinedResults += `Match Context Date: ${new Date().toLocaleDateString()}\n\n`;
296
+ // Add API Data section if available
297
+ if (apiData && coverage.score > 0) {
298
+ combinedResults += `--- API DATA (Reliable Source) ---\n\n`;
299
+ if (apiData.h2h) {
300
+ combinedResults += `### Head-to-Head Record\n`;
301
+ combinedResults += `- Total Matches: ${apiData.h2h.totalMatches}\n`;
302
+ combinedResults += `- ${homeTeam} Wins: ${apiData.h2h.homeWins}\n`;
303
+ combinedResults += `- Draws: ${apiData.h2h.draws}\n`;
304
+ combinedResults += `- ${awayTeam} Wins: ${apiData.h2h.awayWins}\n`;
305
+ combinedResults += `- Goals: ${apiData.h2h.homeGoals}-${apiData.h2h.awayGoals}\n\n`;
306
+ }
307
+ if (apiData.homeForm?.length) {
308
+ combinedResults += `### ${homeTeam} Recent Form (Last ${apiData.homeForm.length})\n`;
309
+ combinedResults += apiData.homeForm.map(m => {
310
+ const isHome = m.homeTeam.name.includes(homeTeam);
311
+ const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
312
+ const opponent = isHome ? m.awayTeam.name : m.homeTeam.name;
313
+ const result = isHome
314
+ ? (m.score?.home ?? 0) > (m.score?.away ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L')
315
+ : (m.score?.away ?? 0) > (m.score?.home ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L');
316
+ return `- ${result} vs ${opponent} (${score})`;
317
+ }).join('\n');
318
+ combinedResults += '\n\n';
319
+ }
320
+ if (apiData.awayForm?.length) {
321
+ combinedResults += `### ${awayTeam} Recent Form (Last ${apiData.awayForm.length})\n`;
322
+ combinedResults += apiData.awayForm.map(m => {
323
+ const isAway = m.awayTeam.name.includes(awayTeam);
324
+ const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
325
+ const opponent = isAway ? m.homeTeam.name : m.awayTeam.name;
326
+ const result = isAway
327
+ ? (m.score?.away ?? 0) > (m.score?.home ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L')
328
+ : (m.score?.home ?? 0) > (m.score?.away ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L');
329
+ return `- ${result} vs ${opponent} (${score})`;
330
+ }).join('\n');
331
+ combinedResults += '\n\n';
332
+ }
333
+ if (apiData.odds) {
334
+ combinedResults += `### Current Odds (from API)\n`;
335
+ combinedResults += `- Home Win: ${apiData.odds.homeWin.toFixed(2)}\n`;
336
+ combinedResults += `- Draw: ${apiData.odds.draw.toFixed(2)}\n`;
337
+ combinedResults += `- Away Win: ${apiData.odds.awayWin.toFixed(2)}\n`;
338
+ if (apiData.odds.asianHandicap) {
339
+ combinedResults += `- Asian Handicap: ${apiData.odds.asianHandicap.line}\n`;
340
+ }
341
+ combinedResults += '\n';
342
+ }
343
+ combinedResults += `--- END API DATA ---\n\n`;
344
+ }
345
+ if (coverage.factors.missingDataPoints.length > 0) {
346
+ combinedResults += `*Note: Using web search to supplement: ${coverage.factors.missingDataPoints.join(', ')}*\n\n`;
347
+ }
95
348
  const candidateUrls = [];
96
349
  // Execute searches in parallel (in batches)
97
350
  const BATCH_SIZE = 4;
@@ -164,6 +417,225 @@ export async function performMatchAnalysis(homeTeam, awayTeam, league, context)
164
417
  combinedResults += `🏆 FINAL VERDICT:\n - Asian Handicap Leans\n - Goal Line (Over/Under)\n - The "Value Pick"`;
165
418
  return combinedResults;
166
419
  }
420
+ /**
421
+ * Calculate probability range with uncertainty quantification
422
+ */
423
+ function calculateProbabilityRange(baseProbability, dataQuality) {
424
+ // Higher data quality = narrower range
425
+ const qualityFactor = dataQuality.score / 100;
426
+ const baseUncertainty = 0.15; // 15% base uncertainty
427
+ // Adjust uncertainty based on data quality
428
+ const adjustedUncertainty = baseUncertainty * (1 - qualityFactor * 0.7);
429
+ // Calculate range
430
+ const mid = baseProbability;
431
+ const margin = adjustedUncertainty * Math.sqrt(baseProbability * (1 - baseProbability));
432
+ return {
433
+ low: Math.max(0, mid - margin * 1.645), // 5th percentile
434
+ mid,
435
+ high: Math.min(1, mid + margin * 1.645), // 95th percentile
436
+ };
437
+ }
438
+ /**
439
+ * Detect value bets from predictions and odds
440
+ */
441
+ async function detectValueBets(apiData, homeTeam, awayTeam) {
442
+ const valueBets = [];
443
+ if (!apiData?.odds)
444
+ return valueBets;
445
+ // Estimate probabilities from API data
446
+ // Simple model: use form and H2H to estimate
447
+ let homeWinProb = 0.33;
448
+ let drawProb = 0.33;
449
+ let awayWinProb = 0.33;
450
+ if (apiData.homeForm && apiData.awayForm) {
451
+ const homeWins = apiData.homeForm.filter(m => {
452
+ const isHome = m.homeTeam.name.includes(homeTeam);
453
+ return isHome ? (m.score?.home ?? 0) > (m.score?.away ?? 0) : (m.score?.away ?? 0) > (m.score?.home ?? 0);
454
+ }).length;
455
+ const awayWins = apiData.awayForm.filter(m => {
456
+ const isAway = m.awayTeam.name.includes(awayTeam);
457
+ return isAway ? (m.score?.away ?? 0) > (m.score?.home ?? 0) : (m.score?.home ?? 0) > (m.score?.away ?? 0);
458
+ }).length;
459
+ // Normalize to probabilities
460
+ const total = homeWins + awayWins + 2; // +2 for draws
461
+ homeWinProb = (homeWins + 1) / total;
462
+ awayWinProb = (awayWins + 1) / total;
463
+ drawProb = 1 - homeWinProb - awayWinProb;
464
+ }
465
+ // Calculate value for each market
466
+ const markets = [
467
+ { selection: `${homeTeam} Win`, prob: homeWinProb, odds: apiData.odds.homeWin },
468
+ { selection: 'Draw', prob: drawProb, odds: apiData.odds.draw },
469
+ { selection: `${awayTeam} Win`, prob: awayWinProb, odds: apiData.odds.awayWin },
470
+ ];
471
+ for (const market of markets) {
472
+ if (!market.odds || market.prob <= 0)
473
+ continue;
474
+ const fairOdds = 1 / market.prob;
475
+ const value = (market.odds / fairOdds) - 1;
476
+ // Only include if value > 5%
477
+ if (value >= 0.05) {
478
+ const kelly = (market.prob * market.odds - 1) / (market.odds - 1);
479
+ valueBets.push({
480
+ selection: market.selection,
481
+ market: '1x2',
482
+ odds: market.odds,
483
+ fairOdds,
484
+ value,
485
+ confidence: Math.min(90, 50 + value * 200), // Scale value to confidence
486
+ kellyFraction: Math.max(0, kelly / 2), // Half Kelly
487
+ recommendedStake: Math.max(0, kelly / 2) * 100, // Based on 100 unit bankroll
488
+ reasoning: `Estimated probability ${(market.prob * 100).toFixed(1)}% vs implied ${((1 / market.odds) * 100).toFixed(1)}%`,
489
+ });
490
+ }
491
+ }
492
+ return valueBets.sort((a, b) => b.value - a.value);
493
+ }
494
+ /**
495
+ * Build structured analysis result from API and web data
496
+ */
497
+ async function buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, coverage) {
498
+ // Calculate base probabilities
499
+ let homeWinProb = 0.35;
500
+ let drawProb = 0.30;
501
+ let awayWinProb = 0.35;
502
+ // Adjust based on form if available
503
+ if (apiData?.homeForm && apiData?.awayForm) {
504
+ const homePoints = apiData.homeForm.reduce((sum, m) => {
505
+ const isHome = m.homeTeam.name.includes(homeTeam);
506
+ const score = m.score;
507
+ if (!score)
508
+ return sum;
509
+ if (isHome) {
510
+ return sum + (score.home > score.away ? 3 : score.home === score.away ? 1 : 0);
511
+ }
512
+ else {
513
+ return sum + (score.away > score.home ? 3 : score.away === score.home ? 1 : 0);
514
+ }
515
+ }, 0);
516
+ const awayPoints = apiData.awayForm.reduce((sum, m) => {
517
+ const isAway = m.awayTeam.name.includes(awayTeam);
518
+ const score = m.score;
519
+ if (!score)
520
+ return sum;
521
+ if (isAway) {
522
+ return sum + (score.away > score.home ? 3 : score.away === score.home ? 1 : 0);
523
+ }
524
+ else {
525
+ return sum + (score.home > score.away ? 3 : score.home === score.away ? 1 : 0);
526
+ }
527
+ }, 0);
528
+ const totalPoints = homePoints + awayPoints + 5; // +5 for draw possibility
529
+ homeWinProb = (homePoints + 1.5) / totalPoints;
530
+ awayWinProb = (awayPoints + 1.5) / totalPoints;
531
+ drawProb = 1 - homeWinProb - awayWinProb;
532
+ }
533
+ // Calculate probability ranges
534
+ const predictions = {
535
+ matchResult: {
536
+ homeWin: calculateProbabilityRange(homeWinProb, coverage),
537
+ draw: calculateProbabilityRange(drawProb, coverage),
538
+ awayWin: calculateProbabilityRange(awayWinProb, coverage),
539
+ },
540
+ overUnder25: {
541
+ over: calculateProbabilityRange(0.50, coverage), // Placeholder
542
+ under: calculateProbabilityRange(0.50, coverage),
543
+ },
544
+ btts: {
545
+ yes: calculateProbabilityRange(0.50, coverage), // Placeholder
546
+ no: calculateProbabilityRange(0.50, coverage),
547
+ },
548
+ };
549
+ // Detect value bets
550
+ const valueBets = await detectValueBets(apiData, homeTeam, awayTeam);
551
+ // Build six-persona analysis
552
+ const analysis = {
553
+ dataScientist: {
554
+ homeWinProbability: predictions.matchResult.homeWin,
555
+ drawProbability: predictions.matchResult.draw,
556
+ awayWinProbability: predictions.matchResult.awayWin,
557
+ expectedGoals: { home: 1.3, away: 1.2 }, // Placeholder - would need xG data
558
+ keyStats: [],
559
+ },
560
+ tacticalScout: {
561
+ styleMatchup: 'Analysis requires detailed tactical data',
562
+ keyBattles: [],
563
+ tacticalAdvantage: 'neutral',
564
+ },
565
+ physio: {
566
+ homeFatigue: {
567
+ score: 50,
568
+ daysRest: 3,
569
+ squadDepth: 5,
570
+ keyInjuries: [],
571
+ },
572
+ awayFatigue: {
573
+ score: 50,
574
+ daysRest: 3,
575
+ squadDepth: 5,
576
+ keyInjuries: [],
577
+ },
578
+ injuryImpact: 'Analysis requires injury data',
579
+ },
580
+ setPieceAnalyst: {
581
+ homeStrength: 5,
582
+ awayStrength: 5,
583
+ predictedCorners: 10,
584
+ aerialAdvantage: 'neutral',
585
+ },
586
+ insider: {
587
+ marketMovement: apiData?.odds ? `Home: ${apiData.odds.homeWin.toFixed(2)}, Draw: ${apiData.odds.draw.toFixed(2)}, Away: ${apiData.odds.awayWin.toFixed(2)}` : 'No odds data',
588
+ },
589
+ skeptic: {
590
+ upsetRisk: 0.25,
591
+ trapIndicators: [],
592
+ gameScenarios: [],
593
+ },
594
+ };
595
+ // Determine best bet
596
+ const bestBet = valueBets.length > 0
597
+ ? valueBets[0].selection
598
+ : `${homeTeam} Win or Draw (Double Chance)`;
599
+ return {
600
+ match: {
601
+ homeTeam,
602
+ awayTeam,
603
+ league,
604
+ matchId: apiData?.match?.id,
605
+ },
606
+ dataQuality: coverage,
607
+ analysis,
608
+ predictions,
609
+ valueBets,
610
+ verdict: {
611
+ summary: `Analysis based on ${coverage.score}/100 data quality. ${apiData?.h2h ? `H2H: ${apiData.h2h.homeWins}-${apiData.h2h.draws}-${apiData.h2h.awayWins}` : 'No H2H data'}`,
612
+ bestBet,
613
+ confidence: coverage.score,
614
+ riskLevel: coverage.score >= 70 ? 'low' : coverage.score >= 40 ? 'medium' : 'high',
615
+ },
616
+ metadata: {
617
+ generatedAt: new Date(),
618
+ dataSources: apiData ? ['api-football'] : ['web-search'],
619
+ },
620
+ };
621
+ }
622
+ /**
623
+ * Perform structured match analysis with JSON output
624
+ */
625
+ export async function performStructuredMatchAnalysis(homeTeam, awayTeam, league, context) {
626
+ // Fetch API data
627
+ const api = createAPIProvider();
628
+ const matchId = await findMatchId(homeTeam, awayTeam, league);
629
+ let apiData = null;
630
+ if (matchId) {
631
+ apiData = await fetchAPIMatchData(api, matchId, homeTeam, awayTeam);
632
+ }
633
+ // Assess data coverage
634
+ const coverage = assessDataCoverage(apiData);
635
+ // Build structured analysis
636
+ const result = await buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, coverage);
637
+ return result;
638
+ }
167
639
  // ============= Tool Registration =============
168
640
  export function registerMatchTools(server) {
169
641
  /**
@@ -211,7 +683,60 @@ Market Intelligence, Referee stats, Weather, Fatigue analysis, Set Pieces.`, {
211
683
  };
212
684
  });
213
685
  /**
214
- * Tool 2: get_live_scores
686
+ * Tool 2: analyze_football_match_structured
687
+ * Structured JSON output for programmatic use
688
+ */
689
+ server.tool('analyze_football_match_structured', `Comprehensive football match analysis with structured JSON output.
690
+ Returns: Probabilities with confidence ranges, 6-persona analysis, value bets, data quality score.
691
+ Use this for programmatic access to match analysis.`, {
692
+ homeTeam: z.string().describe('Name of the home team'),
693
+ awayTeam: z.string().describe('Name of the away team'),
694
+ league: z.string().optional().describe('League name (optional, helps with accuracy)'),
695
+ includeOdds: z.boolean().optional().default(true).describe('Include current odds in analysis'),
696
+ }, async ({ homeTeam, awayTeam, league, includeOdds }) => {
697
+ try {
698
+ const result = await performStructuredMatchAnalysis(homeTeam, awayTeam, league);
699
+ // Format as readable JSON with summary
700
+ const output = {
701
+ summary: `${homeTeam} vs ${awayTeam}`,
702
+ dataQuality: `${result.dataQuality.score}/100`,
703
+ predictions: {
704
+ homeWin: `${(result.predictions.matchResult.homeWin.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.homeWin.low * 100).toFixed(1)}%-${(result.predictions.matchResult.homeWin.high * 100).toFixed(1)}%)`,
705
+ draw: `${(result.predictions.matchResult.draw.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.draw.low * 100).toFixed(1)}%-${(result.predictions.matchResult.draw.high * 100).toFixed(1)}%)`,
706
+ awayWin: `${(result.predictions.matchResult.awayWin.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.awayWin.low * 100).toFixed(1)}%-${(result.predictions.matchResult.awayWin.high * 100).toFixed(1)}%)`,
707
+ },
708
+ valueBets: result.valueBets.length > 0
709
+ ? result.valueBets.map(vb => ({
710
+ selection: vb.selection,
711
+ odds: vb.odds.toFixed(2),
712
+ value: `${(vb.value * 100).toFixed(1)}%`,
713
+ stake: `${vb.recommendedStake.toFixed(1)}u`
714
+ }))
715
+ : 'No value bets detected',
716
+ bestBet: result.verdict.bestBet,
717
+ confidence: `${result.verdict.confidence}/100`,
718
+ riskLevel: result.verdict.riskLevel,
719
+ fullAnalysis: result,
720
+ };
721
+ return {
722
+ content: [{
723
+ type: 'text',
724
+ text: JSON.stringify(output, null, 2)
725
+ }],
726
+ };
727
+ }
728
+ catch (error) {
729
+ return {
730
+ content: [{
731
+ type: 'text',
732
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
733
+ }],
734
+ isError: true,
735
+ };
736
+ }
737
+ });
738
+ /**
739
+ * Tool 3: get_live_scores
215
740
  * Get live football scores for a league or team
216
741
  */
217
742
  server.tool('get_live_scores', `Get live football scores.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "10000.1.2",
3
+ "version": "10000.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,96 +0,0 @@
1
- /**
2
- * ALERT MANAGER
3
- * Real-time alert system with rule engine
4
- */
5
- import { EventEmitter } from 'events';
6
- import type { LiveEvent } from './realtime-manager.js';
7
- export interface AlertRule {
8
- id: string;
9
- name: string;
10
- type: AlertType;
11
- condition: AlertCondition;
12
- channels: NotificationChannel[];
13
- cooldown: number;
14
- enabled: boolean;
15
- createdAt: number;
16
- lastTriggered?: number;
17
- triggerCount?: number;
18
- }
19
- export type AlertType = 'odds_drop' | 'odds_value' | 'goal' | 'red_card' | 'lineup_change' | 'match_start' | 'match_end' | 'custom';
20
- export type AlertCondition = OddsDropCondition | OddsValueCondition | EventCondition | CompositeCondition;
21
- export interface OddsDropCondition {
22
- type: 'odds_drop';
23
- matchId?: string;
24
- team?: string;
25
- threshold: number;
26
- percentage: number;
27
- }
28
- export interface OddsValueCondition {
29
- type: 'odds_value';
30
- matchId?: string;
31
- minValue: number;
32
- maxOdds?: number;
33
- }
34
- export interface EventCondition {
35
- type: 'event';
36
- matchId?: string;
37
- eventTypes: string[];
38
- }
39
- export interface CompositeCondition {
40
- type: 'composite';
41
- conditions: AlertCondition[];
42
- operator: 'AND' | 'OR';
43
- }
44
- export interface NotificationChannel {
45
- type: 'webhook' | 'email' | 'slack' | 'discord' | 'console';
46
- config: Record<string, any>;
47
- }
48
- export interface AlertMessage {
49
- ruleId: string;
50
- ruleName: string;
51
- type: AlertType;
52
- severity: 'info' | 'warning' | 'critical';
53
- title: string;
54
- body: string;
55
- data: any;
56
- timestamp: number;
57
- }
58
- export declare class AlertManager extends EventEmitter {
59
- private rules;
60
- private checkInterval?;
61
- private alertHistory;
62
- private maxHistorySize;
63
- constructor();
64
- start(): void;
65
- stop(): void;
66
- addRule(rule: Omit<AlertRule, 'id' | 'createdAt' | 'triggerCount'>): AlertRule;
67
- removeRule(ruleId: string): boolean;
68
- getRules(): AlertRule[];
69
- getRule(ruleId: string): AlertRule | undefined;
70
- toggleRule(ruleId: string, enabled: boolean): boolean;
71
- processEvent(event: LiveEvent): void;
72
- private evaluateCondition;
73
- private evaluateOddsDrop;
74
- private evaluateOddsValue;
75
- private evaluateEventCondition;
76
- private evaluateCompositeCondition;
77
- private triggerAlert;
78
- private createAlertMessage;
79
- private determineSeverity;
80
- private formatAlertTitle;
81
- private formatAlertBody;
82
- private sendNotification;
83
- private sendWebhook;
84
- private sendSlack;
85
- private sendDiscord;
86
- private checkScheduledAlerts;
87
- getAlertHistory(limit?: number): AlertMessage[];
88
- clearHistory(): void;
89
- getStats(): {
90
- totalRules: number;
91
- enabledRules: number;
92
- totalAlerts: number;
93
- };
94
- }
95
- export declare function getAlertManager(): AlertManager;
96
- export declare function resetAlertManager(): void;