@gotza02/sequential-thinking 10000.2.0 → 10000.2.1

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.
@@ -73,7 +73,8 @@ export declare abstract class APIProviderBase implements IDataProvider {
73
73
  protected rateLimitUntil: Date | null;
74
74
  protected lastCall: Date | null;
75
75
  protected callCount: number;
76
- constructor(type: ProviderType, apiKey: string, baseUrl: string, rateLimit?: number);
76
+ constructor(type: ProviderType, apiKey: string, baseUrl: string, rateLimit?: number, // calls per minute
77
+ cache?: CacheService);
77
78
  /**
78
79
  * Check if provider has valid credentials
79
80
  */
@@ -131,7 +132,7 @@ export declare abstract class APIProviderBase implements IDataProvider {
131
132
  export declare abstract class ScraperProviderBase implements IDataProvider {
132
133
  protected cache: CacheService;
133
134
  readonly type: ProviderType;
134
- constructor();
135
+ constructor(cache?: CacheService);
135
136
  isAvailable(): boolean;
136
137
  getStatus(): ProviderStatus;
137
138
  abstract getMatch(matchId: string): Promise<APIResponse<Match>>;
@@ -2,7 +2,7 @@
2
2
  * SPORTS MODULE BASE CLASSES
3
3
  * Abstract base classes and interfaces for data providers
4
4
  */
5
- import { CacheService } from './cache.js';
5
+ import { CacheService, getGlobalCache } from './cache.js';
6
6
  import { logger } from '../../../utils.js';
7
7
  /**
8
8
  * Abstract base class for API-based providers
@@ -16,13 +16,13 @@ export class APIProviderBase {
16
16
  rateLimitUntil = null;
17
17
  lastCall = null;
18
18
  callCount = 0;
19
- constructor(type, apiKey, baseUrl, rateLimit = 10 // calls per minute
20
- ) {
19
+ constructor(type, apiKey, baseUrl, rateLimit = 10, // calls per minute
20
+ cache) {
21
21
  this.type = type;
22
22
  this.apiKey = apiKey;
23
23
  this.baseUrl = baseUrl;
24
24
  this.rateLimit = rateLimit;
25
- this.cache = new CacheService();
25
+ this.cache = cache || getGlobalCache();
26
26
  }
27
27
  /**
28
28
  * Check if provider has valid credentials
@@ -86,7 +86,9 @@ export class APIProviderBase {
86
86
  // Handle rate limiting
87
87
  if (response.status === 429) {
88
88
  const retryAfter = response.headers.get('Retry-After');
89
- this.rateLimitUntil = new Date(Date.now() + (retryAfter ? parseInt(retryAfter) * 1000 : 60000));
89
+ const retrySeconds = retryAfter ? parseInt(retryAfter, 10) : NaN;
90
+ const delayMs = isNaN(retrySeconds) ? 60000 : retrySeconds * 1000;
91
+ this.rateLimitUntil = new Date(Date.now() + delayMs);
90
92
  return {
91
93
  success: false,
92
94
  error: 'Rate limited',
@@ -127,8 +129,8 @@ export class APIProviderBase {
127
129
  export class ScraperProviderBase {
128
130
  cache;
129
131
  type = 'scraper';
130
- constructor() {
131
- this.cache = new CacheService();
132
+ constructor(cache) {
133
+ this.cache = cache || getGlobalCache();
132
134
  }
133
135
  isAvailable() {
134
136
  return true; // Scraping is always "available"
@@ -181,8 +183,8 @@ export class ScraperProviderBase {
181
183
  export class FallbackProvider {
182
184
  providers;
183
185
  cache;
184
- type = 'scraper';
185
- constructor(providers, cache = new CacheService()) {
186
+ type = 'api-football'; // Most common API type
187
+ constructor(providers, cache = getGlobalCache()) {
186
188
  this.providers = providers;
187
189
  this.cache = cache;
188
190
  }
@@ -275,7 +277,7 @@ export class FallbackProvider {
275
277
  export class SportsToolBase {
276
278
  cache;
277
279
  constructor(cache) {
278
- this.cache = cache || new CacheService();
280
+ this.cache = cache || getGlobalCache();
279
281
  }
280
282
  /**
281
283
  * Format error message for user
@@ -12,7 +12,12 @@ export declare class CacheService {
12
12
  private maxSize;
13
13
  private savePending;
14
14
  private saveTimer;
15
+ cleanupInterval: NodeJS.Timeout | null;
15
16
  constructor(cachePath?: string, maxAge?: number, maxSize?: number);
17
+ /**
18
+ * Initialize cache - must be called after construction
19
+ */
20
+ initialize(): Promise<void>;
16
21
  /**
17
22
  * Generate a cache key from components
18
23
  */
@@ -37,6 +42,10 @@ export declare class CacheService {
37
42
  * Clear all cache entries
38
43
  */
39
44
  clear(): void;
45
+ /**
46
+ * Stop cleanup interval and clear all resources
47
+ */
48
+ dispose(): void;
40
49
  /**
41
50
  * Get cache statistics
42
51
  */
@@ -16,11 +16,17 @@ export class CacheService {
16
16
  maxSize;
17
17
  savePending = false;
18
18
  saveTimer = null;
19
+ cleanupInterval = null;
19
20
  constructor(cachePath = CACHE_CONFIG.CACHE_PATH, maxAge = CACHE_CONFIG.DEFAULT_TTL, maxSize = CACHE_CONFIG.MAX_SIZE) {
20
21
  this.cachePath = path.resolve(cachePath);
21
22
  this.maxAge = maxAge;
22
23
  this.maxSize = maxSize;
23
- this.loadFromFile();
24
+ }
25
+ /**
26
+ * Initialize cache - must be called after construction
27
+ */
28
+ async initialize() {
29
+ await this.loadFromFile();
24
30
  }
25
31
  /**
26
32
  * Generate a cache key from components
@@ -95,6 +101,20 @@ export class CacheService {
95
101
  logger.info('Cache cleared');
96
102
  this.scheduleSave();
97
103
  }
104
+ /**
105
+ * Stop cleanup interval and clear all resources
106
+ */
107
+ dispose() {
108
+ if (this.cleanupInterval) {
109
+ clearInterval(this.cleanupInterval);
110
+ this.cleanupInterval = null;
111
+ }
112
+ if (this.saveTimer) {
113
+ clearTimeout(this.saveTimer);
114
+ this.saveTimer = null;
115
+ }
116
+ this.cache.clear();
117
+ }
98
118
  /**
99
119
  * Get cache statistics
100
120
  */
@@ -288,8 +308,10 @@ let globalCache = null;
288
308
  export function getGlobalCache() {
289
309
  if (!globalCache) {
290
310
  globalCache = new CacheService();
311
+ // Initialize async (fire and forget - will be ready on next tick)
312
+ globalCache.initialize().catch(err => logger.error('Cache init failed:', err));
291
313
  // Cleanup expired entries every 5 minutes
292
- setInterval(() => {
314
+ globalCache.cleanupInterval = setInterval(() => {
293
315
  globalCache?.cleanup();
294
316
  }, 5 * 60 * 1000);
295
317
  }
@@ -300,7 +322,7 @@ export function getGlobalCache() {
300
322
  */
301
323
  export function resetGlobalCache() {
302
324
  if (globalCache) {
303
- globalCache.clear();
325
+ globalCache.dispose();
304
326
  }
305
327
  globalCache = null;
306
328
  }
@@ -138,7 +138,9 @@ export interface TableEntry {
138
138
  drawn: number;
139
139
  lost: number;
140
140
  goalsFor: number;
141
- against: number;
141
+ goalsAgainst: number;
142
+ /** @deprecated Use goalsAgainst instead */
143
+ against?: number;
142
144
  };
143
145
  awayRecord?: {
144
146
  played: number;
@@ -146,7 +148,9 @@ export interface TableEntry {
146
148
  drawn: number;
147
149
  lost: number;
148
150
  goalsFor: number;
149
- against: number;
151
+ goalsAgainst: number;
152
+ /** @deprecated Use goalsAgainst instead */
153
+ against?: number;
150
154
  };
151
155
  lastFive?: {
152
156
  result: 'W' | 'D' | 'L';
@@ -61,6 +61,10 @@ export declare class FootballDataProvider extends APIProviderBase {
61
61
  export declare class SportsDBProvider extends APIProviderBase {
62
62
  constructor();
63
63
  protected getAuthHeaders(): Record<string, string>;
64
+ /**
65
+ * Override callAPI to inject API key in URL for TheSportsDB
66
+ */
67
+ protected callAPI<T>(endpoint: string, options?: RequestInit): Promise<APIResponse<T>>;
64
68
  protected transformResponse<T>(data: any): T;
65
69
  getMatch(matchId: string): Promise<APIResponse<Match>>;
66
70
  getLiveMatches(): Promise<APIResponse<Match[]>>;
@@ -65,7 +65,8 @@ export class APIFootballProvider extends APIProviderBase {
65
65
  return response;
66
66
  }
67
67
  async getPlayer(playerId) {
68
- const response = await this.callAPI(`/players?id=${playerId}&season=2024`);
68
+ const currentYear = new Date().getFullYear();
69
+ const response = await this.callAPI(`/players?id=${playerId}&season=${currentYear}`);
69
70
  if (response.success && response.data?.response?.[0]) {
70
71
  const player = this.transformPlayer(response.data.response[0]);
71
72
  return { success: true, data: player, provider: 'api-football' };
@@ -81,7 +82,8 @@ export class APIFootballProvider extends APIProviderBase {
81
82
  return response;
82
83
  }
83
84
  async searchPlayers(query) {
84
- const response = await this.callAPI(`/players?search=${encodeURIComponent(query)}&season=2024`);
85
+ const currentYear = new Date().getFullYear();
86
+ const response = await this.callAPI(`/players?search=${encodeURIComponent(query)}&season=${currentYear}`);
85
87
  if (response.success && response.data?.response) {
86
88
  const players = response.data.response.map((p) => this.transformPlayer(p));
87
89
  return { success: true, data: players, provider: 'api-football' };
@@ -156,11 +158,11 @@ export class APIFootballProvider extends APIProviderBase {
156
158
  status: this.mapStatus(data.fixture.status.long),
157
159
  venue: data.fixture.venue?.name,
158
160
  score: data.goals ? {
159
- home: data.goals.home,
160
- away: data.goals.away,
161
- halfTime: data.score.halftime ? {
162
- home: data.score.halftime.home,
163
- away: data.score.halftime.away,
161
+ home: data.goals.home ?? 0,
162
+ away: data.goals.away ?? 0,
163
+ halfTime: data.score?.halftime ? {
164
+ home: data.score.halftime.home ?? 0,
165
+ away: data.score.halftime.away ?? 0,
164
166
  } : undefined,
165
167
  } : undefined,
166
168
  minute: data.fixture.status?.elapsed,
@@ -208,7 +210,7 @@ export class APIFootballProvider extends APIProviderBase {
208
210
  drawn: data.home.draw,
209
211
  lost: data.home.lose,
210
212
  goalsFor: data.home.goals.for,
211
- against: data.home.goals.against,
213
+ goalsAgainst: data.home.goals.against,
212
214
  } : undefined,
213
215
  awayRecord: data.away ? {
214
216
  played: data.away.played,
@@ -216,7 +218,7 @@ export class APIFootballProvider extends APIProviderBase {
216
218
  drawn: data.away.draw,
217
219
  lost: data.away.lose,
218
220
  goalsFor: data.away.goals.for,
219
- against: data.away.goals.against,
221
+ goalsAgainst: data.away.goals.against,
220
222
  } : undefined,
221
223
  };
222
224
  }
@@ -230,6 +232,8 @@ export class APIFootballProvider extends APIProviderBase {
230
232
  return 'postponed';
231
233
  if (s.includes('cancelled'))
232
234
  return 'cancelled';
235
+ if (s.includes('abandoned'))
236
+ return 'abandoned';
233
237
  return 'scheduled';
234
238
  }
235
239
  mapPosition(pos) {
@@ -276,9 +280,17 @@ export class APIFootballProvider extends APIProviderBase {
276
280
  homeGoals += isHomeTeam ? match.score.home : match.score.away;
277
281
  awayGoals += isHomeTeam ? match.score.away : match.score.home;
278
282
  }
283
+ // Determine teams - use first match data if available
284
+ const firstMatch = matches[0];
285
+ const homeTeamObj = firstMatch
286
+ ? (firstMatch.homeTeam.id === homeId ? firstMatch.homeTeam : firstMatch.awayTeam)
287
+ : { id: homeId, name: 'Unknown', country: '' };
288
+ const awayTeamObj = firstMatch
289
+ ? (firstMatch.homeTeam.id === awayId ? firstMatch.homeTeam : firstMatch.awayTeam)
290
+ : { id: awayId, name: 'Unknown', country: '' };
279
291
  return {
280
- homeTeam: matches[0]?.homeTeam.id === homeId ? matches[0].homeTeam : matches[0]?.awayTeam,
281
- awayTeam: matches[0]?.homeTeam.id === awayId ? matches[0].homeTeam : matches[0]?.awayTeam,
292
+ homeTeam: homeTeamObj,
293
+ awayTeam: awayTeamObj,
282
294
  totalMatches: matches.length,
283
295
  homeWins,
284
296
  draws,
@@ -306,23 +318,27 @@ export class APIFootballProvider extends APIProviderBase {
306
318
  // Try to find Asian Handicap odds
307
319
  const ahBet = bookmaker?.bets?.find((b) => b.name?.toLowerCase().includes('asian') ||
308
320
  b.name?.toLowerCase().includes('handicap'));
309
- if (ahBet?.values?.[0]) {
310
- const ahValue = ahBet.values[0];
321
+ if (ahBet?.values?.length >= 2) {
322
+ const homeValue = ahBet.values.find((v) => v.value?.toLowerCase().includes('home') || v.value === 'Home');
323
+ const awayValue = ahBet.values.find((v) => v.value?.toLowerCase().includes('away') || v.value === 'Away');
324
+ const lineMatch = ahBet.values[0].value?.match(/[-+]?\d+(?:\.\d+)?/);
311
325
  result.asianHandicap = {
312
- home: parseFloat(ahValue.odd) || 2.0,
313
- away: parseFloat(ahValue.odd) || 2.0,
314
- line: parseFloat(ahValue.value) || 0,
326
+ home: parseFloat(homeValue?.odd) || parseFloat(ahBet.values[0]?.odd) || 2.0,
327
+ away: parseFloat(awayValue?.odd) || parseFloat(ahBet.values[1]?.odd) || 2.0,
328
+ line: lineMatch ? parseFloat(lineMatch[0]) : 0,
315
329
  };
316
330
  }
317
331
  // Try to find Over/Under odds
318
- const ouBet = bookmaker?.bets?.find((b) => b.name?.toLowerCase().includes('over/under'));
319
- if (ouBet?.values?.[0]) {
320
- const ouValue = ouBet.values[0];
321
- const line = parseFloat(ouValue.value?.replace('Over ', '').replace('Under ', '')) || 2.5;
332
+ const ouBet = bookmaker?.bets?.find((b) => b.name?.toLowerCase().includes('over/under') ||
333
+ b.name?.toLowerCase().includes('goals over/under'));
334
+ if (ouBet?.values?.length >= 2) {
335
+ const overValue = ouBet.values.find((v) => v.value?.toLowerCase().includes('over'));
336
+ const underValue = ouBet.values.find((v) => v.value?.toLowerCase().includes('under'));
337
+ const lineMatch = overValue?.value?.match(/\d+(?:\.\d+)?/);
322
338
  result.overUnder = {
323
- line,
324
- over: parseFloat(ouValue.odd) || 2.0,
325
- under: parseFloat(ouValue.odd) || 2.0,
339
+ line: lineMatch ? parseFloat(lineMatch[0]) : 2.5,
340
+ over: parseFloat(overValue?.odd) || 2.0,
341
+ under: parseFloat(underValue?.odd) || 2.0,
326
342
  };
327
343
  }
328
344
  return result;
@@ -353,7 +369,10 @@ export class FootballDataProvider extends APIProviderBase {
353
369
  return response;
354
370
  }
355
371
  async getLiveMatches(leagueId) {
356
- const response = await this.callAPI('/matches?status=IN_PLAY');
372
+ const endpoint = leagueId
373
+ ? `/competitions/${leagueId}/matches?status=IN_PLAY`
374
+ : '/matches?status=IN_PLAY';
375
+ const response = await this.callAPI(endpoint);
357
376
  if (response.success && response.data?.matches) {
358
377
  const matches = response.data.matches.map((m) => this.transformMatchFD(m));
359
378
  return { success: true, data: matches, provider: 'football-data' };
@@ -467,7 +486,7 @@ export class FootballDataProvider extends APIProviderBase {
467
486
  drawn: data.homeGames.draw,
468
487
  lost: data.homeGames.lost,
469
488
  goalsFor: data.homeGames.goalsFor,
470
- against: data.homeGames.goalsAgainst,
489
+ goalsAgainst: data.homeGames.goalsAgainst,
471
490
  } : undefined,
472
491
  awayRecord: data.awayGames ? {
473
492
  played: data.awayGames.playedGames,
@@ -475,7 +494,7 @@ export class FootballDataProvider extends APIProviderBase {
475
494
  drawn: data.awayGames.draw,
476
495
  lost: data.awayGames.lost,
477
496
  goalsFor: data.awayGames.goalsFor,
478
- against: data.awayGames.goalsAgainst,
497
+ goalsAgainst: data.awayGames.goalsAgainst,
479
498
  } : undefined,
480
499
  };
481
500
  }
@@ -502,7 +521,71 @@ export class SportsDBProvider extends APIProviderBase {
502
521
  }
503
522
  getAuthHeaders() {
504
523
  // TheSportsDB uses API key in URL, not headers
505
- return {};
524
+ return {
525
+ 'Accept': 'application/json',
526
+ };
527
+ }
528
+ /**
529
+ * Override callAPI to inject API key in URL for TheSportsDB
530
+ */
531
+ async callAPI(endpoint, options = {}) {
532
+ // Inject API key into URL for TheSportsDB
533
+ const separator = endpoint.includes('?') ? '&' : '?';
534
+ const urlWithKey = `${this.baseUrl}${endpoint}${separator}api_key=${encodeURIComponent(this.apiKey)}`;
535
+ // Check rate limit
536
+ if (this.isRateLimited()) {
537
+ return {
538
+ success: false,
539
+ error: `Rate limited until ${this.rateLimitUntil}`,
540
+ rateLimited: true,
541
+ };
542
+ }
543
+ // Check availability
544
+ if (!this.isAvailable()) {
545
+ return {
546
+ success: false,
547
+ error: 'Provider not available (missing API key)',
548
+ };
549
+ }
550
+ try {
551
+ const response = await fetch(urlWithKey, {
552
+ ...options,
553
+ headers: {
554
+ 'Accept': 'application/json',
555
+ ...options.headers,
556
+ },
557
+ });
558
+ this.lastCall = new Date();
559
+ this.callCount++;
560
+ if (!response.ok) {
561
+ if (response.status === 429) {
562
+ const retryAfter = response.headers.get('Retry-After');
563
+ this.rateLimitUntil = new Date(Date.now() + (retryAfter ? parseInt(retryAfter) * 1000 : 60000));
564
+ return {
565
+ success: false,
566
+ error: 'Rate limited',
567
+ rateLimited: true,
568
+ };
569
+ }
570
+ return {
571
+ success: false,
572
+ error: `HTTP ${response.status}: ${response.statusText}`,
573
+ };
574
+ }
575
+ const data = await response.json();
576
+ return {
577
+ success: true,
578
+ data: data,
579
+ provider: this.type,
580
+ };
581
+ }
582
+ catch (error) {
583
+ logger.error(`${this.type} API error: ${error}`);
584
+ return {
585
+ success: false,
586
+ error: error instanceof Error ? error.message : String(error),
587
+ };
588
+ }
506
589
  }
507
590
  transformResponse(data) {
508
591
  return data;
@@ -8,19 +8,6 @@ import { calculateValue, calculateKelly, calculateRecommendedStake, oddsToProbab
8
8
  import { formatOdds } from '../utils/formatter.js';
9
9
  import { BETTING } from '../core/constants.js';
10
10
  // ============= Helper Functions =============
11
- /**
12
- * Get league ID from league name
13
- */
14
- function getLeagueId(leagueName) {
15
- const normalized = leagueName.toLowerCase();
16
- for (const [key, value] of Object.entries({ ...BETTING })) {
17
- // Skip non-league entries
18
- if (typeof value !== 'object' || value === null || !('name' in value)) {
19
- continue;
20
- }
21
- }
22
- return undefined;
23
- }
24
11
  /**
25
12
  * Search for odds by match
26
13
  */
@@ -159,13 +146,26 @@ Shows: Fair odds, market odds, value %, confidence, Kelly stake.`, {
159
146
  break;
160
147
  case 'fractional':
161
148
  // Parse fractional odds like "5/2" or "5-2"
162
- const oddsStr = odds.toString();
149
+ const oddsStr = odds.toString().trim();
163
150
  const parts = oddsStr.split(/[-/]/);
164
151
  if (parts.length === 2) {
165
- decimalOdds = (parseFloat(parts[0]) / parseFloat(parts[1])) + 1;
152
+ const numerator = parseFloat(parts[0]);
153
+ const denominator = parseFloat(parts[1]);
154
+ if (!isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {
155
+ decimalOdds = (numerator / denominator) + 1;
156
+ }
157
+ else {
158
+ return {
159
+ content: [{ type: 'text', text: `Error: Invalid fractional odds format "${odds}". Use format like "5/2" or "3/1".` }],
160
+ isError: true,
161
+ };
162
+ }
166
163
  }
167
164
  else {
168
- decimalOdds = odds;
165
+ return {
166
+ content: [{ type: 'text', text: `Error: Invalid fractional odds format "${odds}". Use format like "5/2" or "3/1".` }],
167
+ isError: true,
168
+ };
169
169
  }
170
170
  break;
171
171
  }
@@ -1,11 +1,6 @@
1
1
  /**
2
2
  * SPORTS MODULE TOOLS - League Tools
3
3
  * Tools for league standings and statistics
4
- *
5
- * This file will contain:
6
- * - get_league_standings
7
- * - get_top_scorers
8
- *
9
- * Status: Placeholder for Phase 2 implementation
10
4
  */
11
- export {};
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ export declare function registerLeagueTools(server: McpServer): void;