@gotza02/sequential-thinking 10000.2.0 → 10000.2.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.
Files changed (35) hide show
  1. package/README.md.bak +197 -0
  2. package/dist/.gemini_graph_cache.json.bak +1641 -0
  3. package/dist/graph.d.ts +7 -0
  4. package/dist/graph.js +136 -113
  5. package/dist/intelligent-code.d.ts +8 -0
  6. package/dist/intelligent-code.js +152 -125
  7. package/dist/intelligent-code.test.d.ts +1 -0
  8. package/dist/intelligent-code.test.js +104 -0
  9. package/dist/lib/formatters.d.ts +9 -0
  10. package/dist/lib/formatters.js +119 -0
  11. package/dist/lib/validators.d.ts +45 -0
  12. package/dist/lib/validators.js +232 -0
  13. package/dist/lib.js +23 -265
  14. package/dist/tools/sports/core/base.d.ts +3 -2
  15. package/dist/tools/sports/core/base.js +12 -10
  16. package/dist/tools/sports/core/cache.d.ts +9 -0
  17. package/dist/tools/sports/core/cache.js +25 -3
  18. package/dist/tools/sports/core/types.d.ts +6 -2
  19. package/dist/tools/sports/providers/api.d.ts +4 -0
  20. package/dist/tools/sports/providers/api.js +110 -27
  21. package/dist/tools/sports/tools/betting.js +16 -16
  22. package/dist/tools/sports/tools/league.d.ts +2 -7
  23. package/dist/tools/sports/tools/league.js +198 -8
  24. package/dist/tools/sports/tools/live.js +80 -38
  25. package/dist/tools/sports/tools/match-calculations.d.ts +51 -0
  26. package/dist/tools/sports/tools/match-calculations.js +171 -0
  27. package/dist/tools/sports/tools/match-helpers.d.ts +21 -0
  28. package/dist/tools/sports/tools/match-helpers.js +57 -0
  29. package/dist/tools/sports/tools/match.js +227 -125
  30. package/dist/tools/sports.js +3 -3
  31. package/dist/utils.d.ts +111 -44
  32. package/dist/utils.js +510 -305
  33. package/dist/utils.test.js +3 -3
  34. package/package.json +1 -1
  35. package/CLAUDE.md +0 -231
@@ -10,26 +10,10 @@ 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
12
  import { logger } from '../../../utils.js';
13
- // ============= Helper Functions =============
14
- /**
15
- * Get date context for search queries
16
- */
17
- function getDateContext() {
18
- const now = new Date();
19
- const month = now.toLocaleDateString('en-US', { month: 'long' });
20
- const year = now.getFullYear();
21
- return ` ${month} ${year}`;
22
- }
23
- /**
24
- * Normalize team names for better matching
25
- */
26
- function normalizeTeamName(name) {
27
- return name
28
- .toLowerCase()
29
- .replace(/\b(fc|afc|sc|cf|united|utd|city|town|athletic|albion|rovers|wanderers|olympic|real)\b/g, '')
30
- .replace(/\s+/g, ' ')
31
- .trim();
32
- }
13
+ import { getDateContext, isTeamMatch, getAnalysisInstructions } from './match-helpers.js';
14
+ // Note: calculateAdaptiveTTL, calculateProbabilityRange, calculateFormPoints,
15
+ // calculateExpectedGoals, calculateProbabilitiesFromForm, and getLeagueId
16
+ // are defined locally in this file with specific implementations for match analysis
33
17
  /**
34
18
  * Try to find match ID from API by team names and league
35
19
  */
@@ -48,9 +32,9 @@ async function findMatchId(homeTeam, awayTeam, league) {
48
32
  if (liveResult.success && liveResult.data) {
49
33
  const match = liveResult.data.find(m => {
50
34
  const isHomeMatch = homeIds.includes(m.homeTeam.id) ||
51
- normalizeTeamName(m.homeTeam.name).includes(normalizeTeamName(homeTeam));
35
+ isTeamMatch(homeTeam, m.homeTeam.name);
52
36
  const isAwayMatch = awayIds.includes(m.awayTeam.id) ||
53
- normalizeTeamName(m.awayTeam.name).includes(normalizeTeamName(awayTeam));
37
+ isTeamMatch(awayTeam, m.awayTeam.name);
54
38
  return isHomeMatch && isAwayMatch;
55
39
  });
56
40
  if (match)
@@ -248,7 +232,7 @@ function formatH2HData(h2h, homeTeam, awayTeam) {
248
232
  * Get match result character (W/D/L) for a team
249
233
  */
250
234
  function getMatchResultChar(match, teamName) {
251
- const isHome = match.homeTeam.name.includes(teamName);
235
+ const isHome = isTeamMatch(teamName, match.homeTeam.name);
252
236
  const homeScore = match.score?.home ?? 0;
253
237
  const awayScore = match.score?.away ?? 0;
254
238
  if (isHome) {
@@ -265,7 +249,8 @@ function formatTeamForm(form, teamName, isHomeTeam) {
265
249
  const label = isHomeTeam ? 'Home' : 'Away';
266
250
  let output = `### ${teamName} Recent Form (Last ${form.length})\n`;
267
251
  output += form.map(m => {
268
- const opponent = isHomeTeam ? m.awayTeam.name : m.homeTeam.name;
252
+ const isHomeInMatch = isTeamMatch(teamName, m.homeTeam.name);
253
+ const opponent = isHomeInMatch ? m.awayTeam.name : m.homeTeam.name;
269
254
  const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
270
255
  const result = getMatchResultChar(m, teamName);
271
256
  return `- ${result} vs ${opponent} (${score})`;
@@ -345,65 +330,6 @@ async function scrapeContentWithErrorHandling(url, type) {
345
330
  return `${type.charAt(0).toUpperCase() + type.slice(1)} scrape failed: ${errorMsg}`;
346
331
  }
347
332
  }
348
- /**
349
- * Get analysis framework instructions
350
- */
351
- function getAnalysisInstructions() {
352
- return `INSTRUCTIONS: Act as a World-Class Football Analysis Panel. Provide a deep, non-obvious analysis using this framework:\n\n` +
353
- `1. 📊 THE DATA SCIENTIST (Quantitative):\n - Analyze xG trends & Possession stats.\n - Assess Home/Away variance.\n\n` +
354
- `2. 🧠 THE TACTICAL SCOUT (Qualitative):\n - Stylistic Matchup (Press vs Block).\n - Key Battles.\n\n` +
355
- `3. 🚑 THE PHYSIO (Physical Condition):\n - FATIGUE CHECK: Days rest? Travel distance?\n - Squad Depth: Who has the better bench?\n\n` +
356
- `4. 🎯 SET PIECE ANALYST:\n - Corners/Free Kicks: Strong vs Weak?\n - Who is most likely to score from a header?\n\n` +
357
- `5. 💎 THE INSIDER (External Factors):\n - Market Movements (Odds dropping?).\n - Referee & Weather impact.\n\n` +
358
- `6. 🕵️ THE SKEPTIC & SCENARIOS:\n - Why might the favorite LOSE?\n - Game Script: "If Team A scores first..."\n\n` +
359
- `🏆 FINAL VERDICT:\n - Asian Handicap Leans\n - Goal Line (Over/Under)\n - The "Value Pick"`;
360
- }
361
- /**
362
- * Calculate adaptive TTL based on data type and match timing
363
- */
364
- function calculateAdaptiveTTL(dataType, matchDate) {
365
- const now = Date.now();
366
- const hoursUntilMatch = matchDate
367
- ? (matchDate.getTime() - now) / (1000 * 60 * 60)
368
- : Infinity;
369
- switch (dataType) {
370
- case 'odds':
371
- // Odds change frequently - short TTL
372
- if (hoursUntilMatch < 1)
373
- return 60 * 1000; // 1 minute
374
- if (hoursUntilMatch < 24)
375
- return 5 * 60 * 1000; // 5 minutes
376
- return 15 * 60 * 1000; // 15 minutes
377
- case 'lineups':
378
- // Lineups announced ~1 hour before match
379
- if (hoursUntilMatch < 2)
380
- return 5 * 60 * 1000; // 5 minutes
381
- return 60 * 60 * 1000; // 1 hour
382
- case 'news':
383
- // News changes throughout the day
384
- if (hoursUntilMatch < 24)
385
- return 15 * 60 * 1000; // 15 minutes
386
- return 60 * 60 * 1000; // 1 hour
387
- case 'stats':
388
- case 'h2h':
389
- // Historical stats don't change
390
- return 24 * 60 * 60 * 1000; // 24 hours
391
- case 'form':
392
- // Form updates after each match
393
- if (hoursUntilMatch < 0)
394
- return 60 * 1000; // Live match - 1 minute
395
- return 6 * 60 * 60 * 1000; // 6 hours
396
- case 'match':
397
- // Match data changes based on timing
398
- if (hoursUntilMatch < 0)
399
- return 60 * 1000; // Live - 1 minute
400
- if (hoursUntilMatch < 1)
401
- return 5 * 60 * 1000; // 5 minutes
402
- return 30 * 60 * 1000; // 30 minutes
403
- default:
404
- return 30 * 60 * 1000; // 30 minutes default
405
- }
406
- }
407
333
  /**
408
334
  * Perform comprehensive match analysis using web search
409
335
  */
@@ -465,24 +391,6 @@ async function scrapeDeepDiveSources(candidateUrls) {
465
391
  }
466
392
  return output;
467
393
  }
468
- /**
469
- * Calculate probability range with uncertainty quantification
470
- */
471
- function calculateProbabilityRange(baseProbability, dataQuality) {
472
- // Higher data quality = narrower range
473
- const qualityFactor = dataQuality.score / 100;
474
- const baseUncertainty = 0.15; // 15% base uncertainty
475
- // Adjust uncertainty based on data quality
476
- const adjustedUncertainty = baseUncertainty * (1 - qualityFactor * 0.7);
477
- // Calculate range
478
- const mid = baseProbability;
479
- const margin = adjustedUncertainty * Math.sqrt(baseProbability * (1 - baseProbability));
480
- return {
481
- low: Math.max(0, mid - margin * 1.645), // 5th percentile
482
- mid,
483
- high: Math.min(1, mid + margin * 1.645), // 95th percentile
484
- };
485
- }
486
394
  /**
487
395
  * Detect value bets from predictions and odds
488
396
  */
@@ -493,22 +401,33 @@ async function detectValueBets(apiData, homeTeam, awayTeam) {
493
401
  // Estimate probabilities from API data
494
402
  // Simple model: use form and H2H to estimate
495
403
  let homeWinProb = 0.33;
496
- let drawProb = 0.33;
404
+ let drawProb = 0.34;
497
405
  let awayWinProb = 0.33;
498
- if (apiData.homeForm && apiData.awayForm) {
499
- const homeWins = apiData.homeForm.filter(m => {
500
- const isHome = m.homeTeam.name.includes(homeTeam);
501
- return isHome ? (m.score?.home ?? 0) > (m.score?.away ?? 0) : (m.score?.away ?? 0) > (m.score?.home ?? 0);
502
- }).length;
503
- const awayWins = apiData.awayForm.filter(m => {
504
- const isAway = m.awayTeam.name.includes(awayTeam);
505
- return isAway ? (m.score?.away ?? 0) > (m.score?.home ?? 0) : (m.score?.home ?? 0) > (m.score?.away ?? 0);
506
- }).length;
507
- // Normalize to probabilities
508
- const total = homeWins + awayWins + 2; // +2 for draws
509
- homeWinProb = (homeWins + 1) / total;
510
- awayWinProb = (awayWins + 1) / total;
511
- drawProb = 1 - homeWinProb - awayWinProb;
406
+ if (apiData.homeForm?.length && apiData.awayForm?.length) {
407
+ // Calculate form based on points (3 for win, 1 for draw)
408
+ const homePoints = calculateFormPoints(apiData.homeForm, homeTeam);
409
+ const awayPoints = calculateFormPoints(apiData.awayForm, awayTeam);
410
+ // Number of matches played
411
+ const homeMatches = apiData.homeForm.length;
412
+ const awayMatches = apiData.awayForm.length;
413
+ // Points per game
414
+ const homePPG = homePoints / homeMatches;
415
+ const awayPPG = awayPoints / awayMatches;
416
+ // Max PPG is 3, so normalize to probability
417
+ // Give small bonus to home team (home advantage ~5-10%)
418
+ const totalPPG = homePPG + awayPPG;
419
+ if (totalPPG > 0) {
420
+ homeWinProb = Math.min(0.6, (homePPG / totalPPG) * 0.9 + 0.05); // Home advantage
421
+ awayWinProb = Math.min(0.5, (awayPPG / totalPPG) * 0.9);
422
+ // Draw probability based on how close the teams are
423
+ const ppgDiff = Math.abs(homePPG - awayPPG);
424
+ drawProb = Math.max(0.15, 0.35 - ppgDiff * 0.1); // Closer teams = higher draw chance
425
+ // Normalize to ensure sum = 1
426
+ const total = homeWinProb + drawProb + awayWinProb;
427
+ homeWinProb /= total;
428
+ drawProb /= total;
429
+ awayWinProb /= total;
430
+ }
512
431
  }
513
432
  // Calculate value for each market
514
433
  const markets = [
@@ -537,6 +456,100 @@ async function detectValueBets(apiData, homeTeam, awayTeam) {
537
456
  });
538
457
  }
539
458
  }
459
+ // Check Asian Handicap market if available
460
+ if (apiData.odds.asianHandicap) {
461
+ const ah = apiData.odds.asianHandicap;
462
+ // Estimate AH probability based on match probabilities and line
463
+ // Simplified: home wins if they overcome the handicap
464
+ const ahHomeProb = homeWinProb + (drawProb * 0.5); // Approximate
465
+ const ahAwayProb = awayWinProb + (drawProb * 0.5);
466
+ if (ah.home && ahHomeProb > 0) {
467
+ const fairOdds = 1 / ahHomeProb;
468
+ const value = (ah.home / fairOdds) - 1;
469
+ if (value >= 0.05) {
470
+ const kelly = (ahHomeProb * ah.home - 1) / (ah.home - 1);
471
+ valueBets.push({
472
+ selection: `${homeTeam} AH ${ah.line > 0 ? '+' : ''}${ah.line}`,
473
+ market: 'asian_handicap',
474
+ odds: ah.home,
475
+ fairOdds,
476
+ value,
477
+ confidence: Math.min(90, 50 + value * 200),
478
+ kellyFraction: Math.max(0, kelly / 2),
479
+ recommendedStake: Math.max(0, kelly / 2) * 100,
480
+ reasoning: `Line ${ah.line}, estimated prob ${(ahHomeProb * 100).toFixed(1)}%`,
481
+ });
482
+ }
483
+ }
484
+ if (ah.away && ahAwayProb > 0) {
485
+ const fairOdds = 1 / ahAwayProb;
486
+ const value = (ah.away / fairOdds) - 1;
487
+ if (value >= 0.05) {
488
+ const kelly = (ahAwayProb * ah.away - 1) / (ah.away - 1);
489
+ valueBets.push({
490
+ selection: `${awayTeam} AH ${ah.line > 0 ? '-' : '+'}${Math.abs(ah.line)}`,
491
+ market: 'asian_handicap',
492
+ odds: ah.away,
493
+ fairOdds,
494
+ value,
495
+ confidence: Math.min(90, 50 + value * 200),
496
+ kellyFraction: Math.max(0, kelly / 2),
497
+ recommendedStake: Math.max(0, kelly / 2) * 100,
498
+ reasoning: `Line ${-ah.line}, estimated prob ${(ahAwayProb * 100).toFixed(1)}%`,
499
+ });
500
+ }
501
+ }
502
+ }
503
+ // Check Over/Under market if available
504
+ if (apiData.odds.overUnder) {
505
+ const ou = apiData.odds.overUnder;
506
+ // Estimate over probability based on expected goals
507
+ const expectedTotal = (apiData.homeForm?.length && apiData.awayForm?.length)
508
+ ? calculateExpectedGoals(apiData, homeTeam, awayTeam)
509
+ : { home: 1.5, away: 1.2 };
510
+ const totalXG = expectedTotal.home + expectedTotal.away;
511
+ // Simple model: over if total xG > line
512
+ const overProb = totalXG > ou.line
513
+ ? 0.5 + (totalXG - ou.line) * 0.1
514
+ : 0.5 - (ou.line - totalXG) * 0.1;
515
+ const underProb = 1 - overProb;
516
+ if (ou.over && overProb > 0) {
517
+ const fairOdds = 1 / Math.min(0.9, Math.max(0.1, overProb));
518
+ const value = (ou.over / fairOdds) - 1;
519
+ if (value >= 0.05) {
520
+ const kelly = (overProb * ou.over - 1) / (ou.over - 1);
521
+ valueBets.push({
522
+ selection: `Over ${ou.line}`,
523
+ market: 'over_under',
524
+ odds: ou.over,
525
+ fairOdds,
526
+ value,
527
+ confidence: Math.min(90, 50 + value * 200),
528
+ kellyFraction: Math.max(0, kelly / 2),
529
+ recommendedStake: Math.max(0, kelly / 2) * 100,
530
+ reasoning: `Line ${ou.line}, expected goals ${totalXG.toFixed(1)}`,
531
+ });
532
+ }
533
+ }
534
+ if (ou.under && underProb > 0) {
535
+ const fairOdds = 1 / Math.min(0.9, Math.max(0.1, underProb));
536
+ const value = (ou.under / fairOdds) - 1;
537
+ if (value >= 0.05) {
538
+ const kelly = (underProb * ou.under - 1) / (ou.under - 1);
539
+ valueBets.push({
540
+ selection: `Under ${ou.line}`,
541
+ market: 'over_under',
542
+ odds: ou.under,
543
+ fairOdds,
544
+ value,
545
+ confidence: Math.min(90, 50 + value * 200),
546
+ kellyFraction: Math.max(0, kelly / 2),
547
+ recommendedStake: Math.max(0, kelly / 2) * 100,
548
+ reasoning: `Line ${ou.line}, expected goals ${totalXG.toFixed(1)}`,
549
+ });
550
+ }
551
+ }
552
+ }
540
553
  return valueBets.sort((a, b) => b.value - a.value);
541
554
  }
542
555
  /**
@@ -544,7 +557,7 @@ async function detectValueBets(apiData, homeTeam, awayTeam) {
544
557
  */
545
558
  function calculateFormPoints(form, teamName) {
546
559
  return form.reduce((sum, m) => {
547
- const isHome = m.homeTeam.name.includes(teamName);
560
+ const isHome = isTeamMatch(teamName, m.homeTeam.name);
548
561
  const score = m.score;
549
562
  if (!score)
550
563
  return sum;
@@ -558,16 +571,90 @@ function calculateFormPoints(form, teamName) {
558
571
  }, 0);
559
572
  }
560
573
  /**
561
- * Calculate probabilities from form data
574
+ * Calculate expected goals from form data
575
+ * Uses average goals scored and conceded to estimate xG
576
+ */
577
+ function calculateExpectedGoals(apiData, homeTeam, awayTeam) {
578
+ if (!apiData?.homeForm?.length || !apiData?.awayForm?.length) {
579
+ // Default: league average with home advantage
580
+ return { home: 1.5, away: 1.2 };
581
+ }
582
+ // Calculate average goals scored and conceded for home team
583
+ let homeGoalsScored = 0;
584
+ let homeGoalsConceded = 0;
585
+ let homeMatchesWithScores = 0;
586
+ for (const match of apiData.homeForm) {
587
+ if (match.score) {
588
+ const isHomeInMatch = isTeamMatch(homeTeam, match.homeTeam.name);
589
+ homeGoalsScored += isHomeInMatch ? match.score.home : match.score.away;
590
+ homeGoalsConceded += isHomeInMatch ? match.score.away : match.score.home;
591
+ homeMatchesWithScores++;
592
+ }
593
+ }
594
+ // Calculate average goals scored and conceded for away team
595
+ let awayGoalsScored = 0;
596
+ let awayGoalsConceded = 0;
597
+ let awayMatchesWithScores = 0;
598
+ for (const match of apiData.awayForm) {
599
+ if (match.score) {
600
+ const isAwayInMatch = isTeamMatch(awayTeam, match.awayTeam.name);
601
+ awayGoalsScored += isAwayInMatch ? match.score.away : match.score.home;
602
+ awayGoalsConceded += isAwayInMatch ? match.score.home : match.score.away;
603
+ awayMatchesWithScores++;
604
+ }
605
+ }
606
+ // Calculate averages
607
+ const homeScoringAvg = homeMatchesWithScores > 0 ? homeGoalsScored / homeMatchesWithScores : 1.5;
608
+ const homeConcedingAvg = homeMatchesWithScores > 0 ? homeGoalsConceded / homeMatchesWithScores : 1.2;
609
+ const awayScoringAvg = awayMatchesWithScores > 0 ? awayGoalsScored / awayMatchesWithScores : 1.2;
610
+ const awayConcedingAvg = awayMatchesWithScores > 0 ? awayGoalsConceded / awayMatchesWithScores : 1.5;
611
+ // xG estimate using average of "what team scores" and "what opponent concedes"
612
+ // with home advantage adjustment
613
+ const homeAdvantage = 1.15; // ~15% boost for home
614
+ const homeXG = ((homeScoringAvg + awayConcedingAvg) / 2) * homeAdvantage;
615
+ const awayXG = (awayScoringAvg + homeConcedingAvg) / 2;
616
+ // Clamp to reasonable values (0.5 to 3.5 goals)
617
+ return {
618
+ home: Math.min(3.5, Math.max(0.5, homeXG)),
619
+ away: Math.min(3.5, Math.max(0.5, awayXG)),
620
+ };
621
+ }
622
+ /**
623
+ * Calculate probabilities from form data using proper normalization
562
624
  */
563
625
  function calculateProbabilitiesFromForm(homeForm, awayForm, homeTeam, awayTeam) {
564
626
  const homePoints = calculateFormPoints(homeForm, homeTeam);
565
627
  const awayPoints = calculateFormPoints(awayForm, awayTeam);
566
- const totalPoints = homePoints + awayPoints + 5; // +5 for draw possibility
628
+ // Points per game
629
+ const homePPG = homePoints / Math.max(1, homeForm.length);
630
+ const awayPPG = awayPoints / Math.max(1, awayForm.length);
631
+ // Use Bradley-Terry inspired model
632
+ // Convert PPG to relative strength (max 3 points per game)
633
+ const homeStrength = homePPG / 3;
634
+ const awayStrength = awayPPG / 3;
635
+ // Apply home advantage (~10% boost)
636
+ const homeAdvantage = 1.1;
637
+ const effectiveHomeStrength = homeStrength * homeAdvantage;
638
+ // Calculate probabilities
639
+ const totalStrength = effectiveHomeStrength + awayStrength;
640
+ if (totalStrength < 0.1) {
641
+ // Not enough data, return defaults
642
+ return { homeWin: 0.35, draw: 0.30, awayWin: 0.35 };
643
+ }
644
+ // Raw win probabilities (before draw adjustment)
645
+ const rawHomeWin = effectiveHomeStrength / totalStrength;
646
+ const rawAwayWin = awayStrength / totalStrength;
647
+ // Draw probability based on how close teams are
648
+ // When teams are equal, draw probability is higher
649
+ const strengthDiff = Math.abs(homeStrength - awayStrength);
650
+ const drawProb = Math.max(0.15, 0.30 - strengthDiff * 0.3);
651
+ // Adjust win probabilities to account for draw
652
+ const remainingProb = 1 - drawProb;
653
+ const winRatio = rawHomeWin / (rawHomeWin + rawAwayWin);
567
654
  return {
568
- homeWin: (homePoints + 1.5) / totalPoints,
569
- draw: 5 / totalPoints,
570
- awayWin: (awayPoints + 1.5) / totalPoints,
655
+ homeWin: remainingProb * winRatio,
656
+ draw: drawProb,
657
+ awayWin: remainingProb * (1 - winRatio),
571
658
  };
572
659
  }
573
660
  /**
@@ -609,7 +696,7 @@ async function buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, cove
609
696
  homeWinProbability: predictions.matchResult.homeWin,
610
697
  drawProbability: predictions.matchResult.draw,
611
698
  awayWinProbability: predictions.matchResult.awayWin,
612
- expectedGoals: { home: 1.3, away: 1.2 }, // Placeholder - would need xG data
699
+ expectedGoals: calculateExpectedGoals(apiData, homeTeam, awayTeam),
613
700
  keyStats: [],
614
701
  },
615
702
  tacticalScout: {
@@ -821,8 +908,8 @@ Data cached for 1 minute.`, {
821
908
  matches = result.data;
822
909
  // Filter by team if specified
823
910
  if (team) {
824
- matches = matches.filter((m) => m.homeTeam.name.toLowerCase().includes(team.toLowerCase()) ||
825
- m.awayTeam.name.toLowerCase().includes(team.toLowerCase()));
911
+ matches = matches.filter((m) => isTeamMatch(team, m.homeTeam.name) ||
912
+ isTeamMatch(team, m.awayTeam.name));
826
913
  }
827
914
  // Limit results
828
915
  matches = matches.slice(0, limit);
@@ -934,6 +1021,21 @@ Requires API provider with match ID.`, {
934
1021
  });
935
1022
  }
936
1023
  // ============= Helper Functions =============
1024
+ /**
1025
+ * Calculate probability range with uncertainty quantification
1026
+ */
1027
+ function calculateProbabilityRange(baseProbability, dataQuality) {
1028
+ const qualityFactor = dataQuality.score / 100;
1029
+ const baseUncertainty = 0.15;
1030
+ const adjustedUncertainty = baseUncertainty * (1 - qualityFactor * 0.7);
1031
+ const mid = baseProbability;
1032
+ const margin = adjustedUncertainty * Math.sqrt(mid * (1 - mid) + 0.1);
1033
+ return {
1034
+ low: Math.max(0, mid - margin * 1.96),
1035
+ mid: Math.min(1, Math.max(0, mid)),
1036
+ high: Math.min(1, mid + margin * 1.96),
1037
+ };
1038
+ }
937
1039
  /**
938
1040
  * Get league ID from league name
939
1041
  */
@@ -91,7 +91,7 @@ function registerLegacyAnalyzeMatch(server) {
91
91
  const data = await response.json();
92
92
  const items = data.web?.results || [];
93
93
  items.forEach((item) => candidateUrls.push(item.url));
94
- resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.description}`).join('\n');
94
+ resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.description ?? ''}`).join('\n');
95
95
  }
96
96
  else if (searchProvider === 'exa') {
97
97
  const response = await fetchWithRetry('https://api.exa.ai/search', {
@@ -107,7 +107,7 @@ function registerLegacyAnalyzeMatch(server) {
107
107
  const data = await response.json();
108
108
  const items = data.results || [];
109
109
  items.forEach((item) => candidateUrls.push(item.url));
110
- resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.text || item.title}`).join('\n');
110
+ resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.text ?? item.title}`).join('\n');
111
111
  }
112
112
  else if (searchProvider === 'google') {
113
113
  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(q.query)}&num=4`);
@@ -116,7 +116,7 @@ function registerLegacyAnalyzeMatch(server) {
116
116
  const data = await response.json();
117
117
  const items = data.items || [];
118
118
  items.forEach((item) => candidateUrls.push(item.link));
119
- resultsText = items.map((item) => `- [${item.title}](${item.link}): ${item.snippet}`).join('\n');
119
+ resultsText = items.map((item) => `- [${item.title}](${item.link}): ${item.snippet ?? ''}`).join('\n');
120
120
  }
121
121
  return `### ${q.type}\n${resultsText}\n`;
122
122
  }
package/dist/utils.d.ts CHANGED
@@ -1,63 +1,130 @@
1
+ declare const CONFIG: {
2
+ readonly COMMAND_TIMEOUT: 60000;
3
+ readonly MAX_BUFFER: number;
4
+ readonly MAX_PATTERN_LENGTH: 500;
5
+ readonly MAX_QUANTIFIERS: 20;
6
+ readonly MAX_LOCKS: 1000;
7
+ readonly DNS_TIMEOUT: 5000;
8
+ readonly MAX_INPUT_LENGTH: 10000;
9
+ readonly RETRY: {
10
+ readonly MAX_RETRIES: 3;
11
+ readonly BASE_DELAY: 1000;
12
+ readonly BACKOFF_MULTIPLIER: 2;
13
+ };
14
+ };
15
+ /**
16
+ * Whitelist of safe commands. Consider loading from config file for flexibility.
17
+ */
18
+ declare const SAFE_COMMANDS: ReadonlySet<string>;
19
+ export declare class SecurityError extends Error {
20
+ readonly code: string;
21
+ constructor(message: string, code: string);
22
+ }
23
+ export declare class ValidationError extends Error {
24
+ readonly field?: string | undefined;
25
+ constructor(message: string, field?: string | undefined);
26
+ }
27
+ export declare class TimeoutError extends Error {
28
+ readonly timeout: number;
29
+ constructor(message: string, timeout: number);
30
+ }
31
+ interface ParsedCommand {
32
+ command: string;
33
+ args: string[];
34
+ }
1
35
  /**
2
- * Execute a shell command with security protections against command injection.
3
- * Uses spawn() instead of exec() to avoid shell interpretation.
4
- *
5
- * @param command - The command string to execute
6
- * @param options - Execution options (timeout, maxBuffer)
7
- * @returns Promise with stdout and stderr
8
- * @throws Error if command contains shell metacharacters or is not whitelisted
36
+ * Robust command parser supporting quotes and escapes.
37
+ * State machine implementation for correctness.
9
38
  */
10
- export declare function execAsync(command: string, options?: {
39
+ declare function parseCommand(command: string): ParsedCommand;
40
+ export interface ExecOptions {
11
41
  timeout?: number;
12
42
  maxBuffer?: number;
13
- }): Promise<{
43
+ cwd?: string;
44
+ env?: NodeJS.ProcessEnv;
45
+ }
46
+ export interface ExecResult {
14
47
  stdout: string;
15
48
  stderr: string;
16
- }>;
49
+ exitCode: number;
50
+ executionTime: number;
51
+ }
52
+ /**
53
+ * Execute command securely using spawn (no shell interpretation).
54
+ */
55
+ export declare function execAsync(command: string, options?: ExecOptions): Promise<ExecResult>;
17
56
  export declare class AsyncMutex {
18
- private mutex;
19
- lock(): Promise<() => void>;
20
- dispatch<T>(fn: (() => T | PromiseLike<T>)): Promise<T>;
57
+ private queue;
58
+ private locked;
59
+ acquire(): Promise<() => void>;
60
+ private release;
61
+ dispatch<T>(fn: () => T | PromiseLike<T>): Promise<T>;
62
+ }
63
+ export declare class FileLock {
64
+ private locks;
65
+ private queue;
66
+ private readonly maxLocks;
67
+ constructor(maxLocks?: 1000);
68
+ acquire(filePath: string, timeout?: number): Promise<{
69
+ release: () => void;
70
+ }>;
71
+ private waitForLock;
72
+ private release;
73
+ private cleanupStaleLocks;
74
+ getLockCount(): number;
75
+ getQueueCount(): number;
21
76
  }
77
+ export declare const fileLock: FileLock;
22
78
  export declare function validatePath(requestedPath: string): string;
79
+ declare function isPrivateIPv4(ip: string): boolean;
80
+ declare function isPrivateIPv6(ip: string): boolean;
23
81
  export declare function validatePublicUrl(urlString: string): Promise<void>;
24
82
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
83
+ interface LoggerOptions {
84
+ level?: LogLevel;
85
+ useColors?: boolean;
86
+ output?: 'stdout' | 'stderr';
87
+ }
25
88
  declare class Logger {
26
89
  private level;
27
- constructor();
90
+ private useColors;
91
+ private output;
92
+ private static readonly LEVELS;
93
+ private static readonly COLORS;
94
+ constructor(options?: LoggerOptions);
28
95
  private shouldLog;
29
- debug(msg: string, ...args: any[]): void;
30
- info(msg: string, ...args: any[]): void;
31
- warn(msg: string, ...args: any[]): void;
32
- error(msg: string, ...args: any[]): void;
96
+ private formatMessage;
97
+ private print;
98
+ debug(msg: string, ...args: unknown[]): void;
99
+ info(msg: string, ...args: unknown[]): void;
100
+ warn(msg: string, ...args: unknown[]): void;
101
+ error(msg: string, ...args: unknown[]): void;
102
+ setLevel(level: LogLevel): void;
33
103
  }
34
104
  export declare const logger: Logger;
35
- /**
36
- * Validate a regex pattern for ReDoS vulnerabilities
37
- * @throws Error if pattern is unsafe
38
- */
105
+ export declare class ReDoSError extends SecurityError {
106
+ constructor(message: string);
107
+ }
39
108
  export declare function validateRegexPattern(pattern: string): void;
40
- /**
41
- * Create a RegExp object with ReDoS protection
42
- * @param pattern - The regex pattern
43
- * @param flags - Regex flags (g, i, m, etc.)
44
- * @returns Safe RegExp object
45
- * @throws Error if pattern is unsafe
46
- */
47
109
  export declare function createSafeRegex(pattern: string, flags?: string): RegExp;
48
- export declare const DEFAULT_HEADERS: {
49
- 'User-Agent': string;
50
- Accept: string;
51
- 'Accept-Language': string;
52
- };
53
- export declare function fetchWithRetry(url: string, options?: any, retries?: number, backoff?: number): Promise<Response>;
54
- export declare function withRetry(fn: () => Promise<any>, maxRetries?: number, baseDelay?: number): Promise<any>;
55
- export declare class FileLock {
56
- locks: Map<any, any>;
57
- acquire(filePath: string, timeout?: number): Promise<{
58
- release: () => void;
59
- }>;
110
+ export declare const DEFAULT_HEADERS: Readonly<Record<string, string>>;
111
+ export interface FetchOptions extends RequestInit {
112
+ retries?: number;
113
+ backoff?: number;
114
+ validateUrl?: boolean;
60
115
  }
61
- export declare const fileLock: FileLock;
62
- export declare function sanitizeInput(input: any, maxLength?: number): string;
63
- export {};
116
+ export declare function fetchWithRetry(url: string, options?: FetchOptions): Promise<Response>;
117
+ export declare function withRetry<T>(fn: () => Promise<T>, options?: {
118
+ maxRetries?: number;
119
+ baseDelay?: number;
120
+ onRetry?: (error: Error, attempt: number) => void;
121
+ shouldRetry?: (error: Error) => boolean;
122
+ }): Promise<T>;
123
+ export interface SanitizeOptions {
124
+ maxLength?: number;
125
+ allowedPattern?: RegExp;
126
+ removeNull?: boolean;
127
+ trim?: boolean;
128
+ }
129
+ export declare function sanitizeInput(input: unknown, options?: SanitizeOptions): string;
130
+ export { CONFIG, SAFE_COMMANDS, parseCommand, isPrivateIPv4, isPrivateIPv6, Logger, };