@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.
@@ -1,12 +1,202 @@
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 {};
12
- // Placeholder - Tools will be implemented in Phase 2
5
+ import { z } from 'zod';
6
+ import { createAPIProvider } from '../providers/api.js';
7
+ import { getSearchProvider } from '../providers/search.js';
8
+ import { getGlobalCache, CacheService } from '../core/cache.js';
9
+ import { formatStandingsTable } from '../utils/formatter.js';
10
+ import { CACHE_CONFIG, LEAGUES } from '../core/constants.js';
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(LEAGUES)) {
17
+ if (value.name.toLowerCase().includes(normalized) ||
18
+ normalized.includes(value.name.toLowerCase()) ||
19
+ value.code?.toLowerCase() === normalized.replace(' ', '')) {
20
+ return value.id;
21
+ }
22
+ }
23
+ // Try exact match
24
+ for (const [key, value] of Object.entries(LEAGUES)) {
25
+ if (value.name.toLowerCase() === normalized) {
26
+ return value.id;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+ // ============= Tool Registration =============
32
+ export function registerLeagueTools(server) {
33
+ /**
34
+ * Tool 1: get_league_standings
35
+ * Get current league standings table
36
+ */
37
+ server.tool('get_league_standings_v2', `Get current league standings table.
38
+ Includes: position, played, won, drawn, lost, goals for/against, goal difference, points.
39
+ Supports home/away splits.`, {
40
+ league: z.string().describe('League name (e.g., "Premier League", "La Liga")'),
41
+ season: z.string().optional().describe('Season year (default: current)'),
42
+ }, async ({ league, season }) => {
43
+ const cache = getGlobalCache();
44
+ const currentYear = new Date().getFullYear();
45
+ const seasonYear = season || currentYear.toString();
46
+ const cacheKey = CacheService.generateKey('standings', league, seasonYear);
47
+ // Check cache first (30min TTL)
48
+ const cached = cache.get(cacheKey);
49
+ if (cached) {
50
+ return {
51
+ content: [{ type: 'text', text: cached + '\n\n*(cached data)*' }],
52
+ };
53
+ }
54
+ const leagueId = getLeagueId(league);
55
+ let output = '';
56
+ const errors = [];
57
+ if (!leagueId) {
58
+ errors.push(`League "${league}" not found. Available leagues: ${Object.values(LEAGUES).map(l => l.name).join(', ')}`);
59
+ }
60
+ if (leagueId) {
61
+ try {
62
+ const api = createAPIProvider();
63
+ const result = await api.getStandings(leagueId.toString(), seasonYear);
64
+ if (result.success && result.data) {
65
+ output = `## 🏆 ${league} - Standings ${seasonYear}\n\n`;
66
+ output += formatStandingsTable(result.data);
67
+ // Cache for 30 minutes
68
+ cache.set(cacheKey, output, CACHE_CONFIG.TTL.STANDINGS);
69
+ }
70
+ else {
71
+ errors.push(result.error || 'Unknown API error');
72
+ }
73
+ }
74
+ catch (error) {
75
+ errors.push(error instanceof Error ? error.message : String(error));
76
+ }
77
+ }
78
+ // Fallback to web search if API failed
79
+ if (!output && errors.length > 0) {
80
+ try {
81
+ const searchQuery = `${league} standings table ${seasonYear}`;
82
+ const searchProvider = getSearchProvider();
83
+ const searchResults = await searchProvider.search(searchQuery, undefined, 3);
84
+ const fallbackText = searchResults.map(r => `- [${r.title}](${r.url}): ${r.snippet}`).join('\n');
85
+ output = `## ${league} - Standings ${seasonYear}\n\n`;
86
+ output += `*API providers not available. Click links for standings:*\n\n${fallbackText}`;
87
+ }
88
+ catch (searchError) {
89
+ output = `Error: ${errors.join(', ')}`;
90
+ }
91
+ }
92
+ return {
93
+ content: [{ type: 'text', text: output }],
94
+ };
95
+ });
96
+ /**
97
+ * Tool 2: get_top_scorers
98
+ * Get top scorers for a league
99
+ */
100
+ server.tool('get_top_scorers_v2', `Get top scorers for a league.`, {
101
+ league: z.string().describe('League name'),
102
+ limit: z.number().optional().default(20).describe('Number of players to return'),
103
+ season: z.string().optional().describe('Season year (default: current)'),
104
+ }, async ({ league, limit, season }) => {
105
+ const cache = getGlobalCache();
106
+ const currentYear = new Date().getFullYear();
107
+ const seasonYear = season || currentYear.toString();
108
+ const cacheKey = CacheService.generateKey('scorers', league, seasonYear, limit.toString());
109
+ // Check cache first
110
+ const cached = cache.get(cacheKey);
111
+ if (cached) {
112
+ return {
113
+ content: [{ type: 'text', text: cached + '\n\n*(cached data)*' }],
114
+ };
115
+ }
116
+ const leagueId = getLeagueId(league);
117
+ let output = '';
118
+ const errors = [];
119
+ if (!leagueId) {
120
+ errors.push(`League "${league}" not found`);
121
+ }
122
+ // Try API first
123
+ if (leagueId) {
124
+ try {
125
+ const api = createAPIProvider();
126
+ const searchQuery = `${league} top scorers ${seasonYear}`;
127
+ const searchProvider = getSearchProvider();
128
+ const searchResults = await searchProvider.search(searchQuery, undefined, limit);
129
+ output = `## 🥇 ${league} - Top Scorers ${seasonYear}\n\n`;
130
+ if (searchResults.length > 0) {
131
+ const items = searchResults.map((r, i) => `${i + 1}. [${r.title}](${r.url}): ${r.snippet}`).join('\n');
132
+ output += items;
133
+ }
134
+ else {
135
+ output += 'No results found.';
136
+ }
137
+ // Cache the results
138
+ cache.set(cacheKey, output, CACHE_CONFIG.TTL.TEAM_STATS);
139
+ }
140
+ catch (error) {
141
+ errors.push(error instanceof Error ? error.message : String(error));
142
+ }
143
+ }
144
+ // Fallback
145
+ if (!output) {
146
+ try {
147
+ const searchQuery = `${league} top scorers ${seasonYear}`;
148
+ const searchProvider = getSearchProvider();
149
+ const searchResults = await searchProvider.search(searchQuery, undefined, limit);
150
+ output = `## 🥇 ${league} - Top Scorers ${seasonYear}\n\n`;
151
+ output += searchResults.map((r, i) => `${i + 1}. [${r.title}](${r.url}): ${r.snippet}`).join('\n');
152
+ }
153
+ catch (error) {
154
+ output = `Error: ${errors.join(', ')}`;
155
+ }
156
+ }
157
+ return {
158
+ content: [{ type: 'text', text: output }],
159
+ };
160
+ });
161
+ /**
162
+ * Tool 3: get_league_fixtures
163
+ * Get upcoming fixtures for a league
164
+ */
165
+ server.tool('get_league_fixtures', `Get upcoming fixtures for a league.`, {
166
+ league: z.string().describe('League name'),
167
+ days: z.number().optional().default(7).describe('Number of days ahead'),
168
+ }, async ({ league, days }) => {
169
+ const cache = getGlobalCache();
170
+ const cacheKey = CacheService.generateKey('fixtures', league, days.toString());
171
+ // Check cache first
172
+ const cached = cache.get(cacheKey);
173
+ if (cached) {
174
+ return {
175
+ content: [{ type: 'text', text: cached + '\n\n*(cached data)*' }],
176
+ };
177
+ }
178
+ const leagueId = getLeagueId(league);
179
+ let output = '';
180
+ try {
181
+ const searchQuery = `${league} fixtures schedule next ${days} days`;
182
+ const searchProvider = getSearchProvider();
183
+ const searchResults = await searchProvider.search(searchQuery, undefined, 10);
184
+ output = `## 📅 ${league} - Upcoming Fixtures\n\n`;
185
+ if (searchResults.length > 0) {
186
+ const items = searchResults.map((r, i) => `${i + 1}. [${r.title}](${r.url}): ${r.snippet}`).join('\n\n');
187
+ output += items;
188
+ }
189
+ else {
190
+ output += 'No fixtures found.';
191
+ }
192
+ // Cache for short period
193
+ cache.set(cacheKey, output, 15 * 60 * 1000); // 15 minutes
194
+ }
195
+ catch (error) {
196
+ output = `Error: ${error instanceof Error ? error.message : String(error)}`;
197
+ }
198
+ return {
199
+ content: [{ type: 'text', text: output }],
200
+ };
201
+ });
202
+ }
@@ -6,34 +6,65 @@ import { z } from 'zod';
6
6
  import * as fs from 'fs/promises';
7
7
  import * as path from 'path';
8
8
  import { getSearchProvider } from '../providers/search.js';
9
+ import { logger } from '../../../utils.js';
9
10
  // Configuration for watchlist persistence
10
11
  const WATCHLIST_FILE = process.env.SPORTS_WATCHLIST_PATH || '.sports_watchlist.json';
11
- // In-memory watchlist storage (initialized from file)
12
- let watchlist = [];
13
- // Load watchlist from file
14
- async function loadWatchlist() {
15
- try {
16
- const filePath = path.resolve(WATCHLIST_FILE);
17
- const data = await fs.readFile(filePath, 'utf-8');
18
- watchlist = JSON.parse(data);
12
+ // Watchlist storage with proper initialization
13
+ class WatchlistStorage {
14
+ items = [];
15
+ initialized = false;
16
+ initPromise = null;
17
+ async initialize() {
18
+ if (this.initialized)
19
+ return;
20
+ // Prevent concurrent initialization
21
+ if (this.initPromise)
22
+ return this.initPromise;
23
+ this.initPromise = this.loadFromFile();
24
+ await this.initPromise;
25
+ this.initialized = true;
19
26
  }
20
- catch (error) {
21
- // File doesn't exist or is invalid, start with empty list
22
- watchlist = [];
27
+ async loadFromFile() {
28
+ try {
29
+ const filePath = path.resolve(WATCHLIST_FILE);
30
+ const data = await fs.readFile(filePath, 'utf-8');
31
+ this.items = JSON.parse(data);
32
+ }
33
+ catch (error) {
34
+ // File doesn't exist or is invalid, start with empty list
35
+ this.items = [];
36
+ }
23
37
  }
24
- }
25
- // Save watchlist to file
26
- async function saveWatchlist() {
27
- try {
28
- const filePath = path.resolve(WATCHLIST_FILE);
29
- await fs.writeFile(filePath, JSON.stringify(watchlist, null, 2), 'utf-8');
38
+ async save() {
39
+ try {
40
+ const filePath = path.resolve(WATCHLIST_FILE);
41
+ await fs.writeFile(filePath, JSON.stringify(this.items, null, 2), 'utf-8');
42
+ }
43
+ catch (error) {
44
+ logger.error(`Failed to save watchlist to ${WATCHLIST_FILE}:`, error);
45
+ }
46
+ }
47
+ getItems() {
48
+ return this.items;
49
+ }
50
+ add(item) {
51
+ this.items.push(item);
52
+ }
53
+ remove(id) {
54
+ const index = this.items.findIndex(item => item.id === id);
55
+ if (index === -1)
56
+ return null;
57
+ return this.items.splice(index, 1)[0];
58
+ }
59
+ clear() {
60
+ this.items = [];
30
61
  }
31
- catch (error) {
32
- console.error(`Failed to save watchlist to ${WATCHLIST_FILE}:`, error);
62
+ get length() {
63
+ return this.items.length;
33
64
  }
34
65
  }
35
- // Initialize watchlist
36
- loadWatchlist();
66
+ // Singleton instance
67
+ const watchlistStorage = new WatchlistStorage();
37
68
  /**
38
69
  * Register live and alert tools
39
70
  */
@@ -58,6 +89,8 @@ Can be called with:
58
89
  awayTeam: z.string().optional().describe('Away team name'),
59
90
  alertCondition: z.string().optional().describe('Alert condition for match'),
60
91
  }, async ({ type, target, condition, matchId, homeTeam, awayTeam, alertCondition }) => {
92
+ // Ensure watchlist is initialized before use
93
+ await watchlistStorage.initialize();
61
94
  const id = `watch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
62
95
  // Handle match mode (matchId, homeTeam, awayTeam provided)
63
96
  let itemType = type;
@@ -88,8 +121,8 @@ Can be called with:
88
121
  createdAt: new Date().toISOString(),
89
122
  triggered: false,
90
123
  };
91
- watchlist.push(item);
92
- await saveWatchlist();
124
+ watchlistStorage.add(item);
125
+ await watchlistStorage.save();
93
126
  let output = `## 📋 Added to Watchlist\n\n`;
94
127
  output += `**ID:** ${id}\n`;
95
128
  output += `**Type:** ${itemType}\n`;
@@ -100,7 +133,7 @@ Can be called with:
100
133
  output += `**Match:** ${homeTeam} vs ${awayTeam}\n`;
101
134
  }
102
135
  output += `**Created:** ${item.createdAt}\n`;
103
- output += `\n\nTotal watchlist items: ${watchlist.length}`;
136
+ output += `\n\nTotal watchlist items: ${watchlistStorage.length}`;
104
137
  return {
105
138
  content: [{ type: 'text', text: output }],
106
139
  };
@@ -110,13 +143,16 @@ Can be called with:
110
143
  * List all items in the watchlist
111
144
  */
112
145
  server.tool('watchlist_list', `List all items in the sports watchlist.`, {}, async () => {
113
- if (watchlist.length === 0) {
146
+ // Ensure watchlist is initialized
147
+ await watchlistStorage.initialize();
148
+ const items = watchlistStorage.getItems();
149
+ if (items.length === 0) {
114
150
  return {
115
151
  content: [{ type: 'text', text: '## 📋 Watchlist\n\nNo items in watchlist.' }],
116
152
  };
117
153
  }
118
- let output = `## 📋 Watchlist (${watchlist.length} items)\n\n`;
119
- for (const item of watchlist) {
154
+ let output = `## 📋 Watchlist (${items.length} items)\n\n`;
155
+ for (const item of items) {
120
156
  const status = item.triggered ? '✅' : '👀';
121
157
  output += `${status} **${item.id}**\n`;
122
158
  output += `- Type: ${item.type}\n`;
@@ -136,20 +172,21 @@ Can be called with:
136
172
  server.tool('watchlist_remove', `Remove an item from the sports watchlist.`, {
137
173
  id: z.string().describe('Watchlist item ID to remove'),
138
174
  }, async ({ id }) => {
139
- const index = watchlist.findIndex(item => item.id === id);
140
- if (index === -1) {
175
+ // Ensure watchlist is initialized
176
+ await watchlistStorage.initialize();
177
+ const removed = watchlistStorage.remove(id);
178
+ if (!removed) {
141
179
  return {
142
180
  content: [{ type: 'text', text: `Error: Watchlist item "${id}" not found.` }],
143
181
  isError: true,
144
182
  };
145
183
  }
146
- const removed = watchlist.splice(index, 1)[0];
147
- await saveWatchlist();
184
+ await watchlistStorage.save();
148
185
  let output = `## 📋 Removed from Watchlist\n\n`;
149
186
  output += `**ID:** ${removed.id}\n`;
150
187
  output += `**Type:** ${removed.type}\n`;
151
188
  output += `**Target:** ${removed.target}\n`;
152
- output += `\n\nRemaining items: ${watchlist.length}`;
189
+ output += `\n\nRemaining items: ${watchlistStorage.length}`;
153
190
  return {
154
191
  content: [{ type: 'text', text: output }],
155
192
  };
@@ -159,9 +196,11 @@ Can be called with:
159
196
  * Clear all items from the watchlist
160
197
  */
161
198
  server.tool('watchlist_clear', `Clear all items from the sports watchlist.`, {}, async () => {
162
- const count = watchlist.length;
163
- watchlist = []; // Reset array
164
- await saveWatchlist();
199
+ // Ensure watchlist is initialized
200
+ await watchlistStorage.initialize();
201
+ const count = watchlistStorage.length;
202
+ watchlistStorage.clear();
203
+ await watchlistStorage.save();
165
204
  return {
166
205
  content: [{ type: 'text', text: `## 📋 Watchlist Cleared\n\nRemoved ${count} item(s).` }],
167
206
  };
@@ -171,8 +210,11 @@ Can be called with:
171
210
  * Check watchlist for triggered alerts
172
211
  */
173
212
  server.tool('check_alerts', `Check watchlist for triggered alerts based on current data.`, {}, async () => {
213
+ // Ensure watchlist is initialized
214
+ await watchlistStorage.initialize();
174
215
  let output = `## 🔔 Alert Check Results\n\n`;
175
- if (watchlist.length === 0) {
216
+ const items = watchlistStorage.getItems();
217
+ if (items.length === 0) {
176
218
  output += 'No items in watchlist.';
177
219
  return {
178
220
  content: [{ type: 'text', text: output }],
@@ -181,7 +223,7 @@ Can be called with:
181
223
  const searchProvider = getSearchProvider();
182
224
  let triggeredCount = 0;
183
225
  let saveNeeded = false;
184
- for (const item of watchlist) {
226
+ for (const item of items) {
185
227
  let triggered = false;
186
228
  let message = '';
187
229
  // Check based on type
@@ -217,7 +259,7 @@ Can be called with:
217
259
  }
218
260
  }
219
261
  if (saveNeeded) {
220
- await saveWatchlist();
262
+ await watchlistStorage.save();
221
263
  }
222
264
  if (triggeredCount === 0) {
223
265
  output += 'No new alerts. All conditions normal.';