@semanticintent/semantic-chirp-intelligence-mcp 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.chirp-data/opponent.json +24 -0
- package/.chirp-data/roster.json +34 -0
- package/.chirp-data/standings.json +17 -0
- package/.nhl-schedule-cache/20262027.json +1 -0
- package/.nhl-schedule-cache/players-20262027-20252026.json +1 -0
- package/README.md +129 -79
- package/build/analyses/BreakoutAnalysis.js +25 -36
- package/build/analyses/DraftPickAnalysis.js +431 -0
- package/build/analyses/GamesInHandAnalysis.js +52 -121
- package/build/analyses/IceAnalysis.js +66 -57
- package/build/analyses/LineupAnalysis.js +26 -46
- package/build/analyses/ScheduleValueAnalysis.js +279 -0
- package/build/analyses/StreamingAnalysis.js +40 -76
- package/build/analyses/WeekendStreamAnalysis.js +130 -66
- package/build/config/tool-metadata.js +54 -0
- package/build/domain/nhl-teams.js +67 -0
- package/build/index.js +561 -653
- package/build/services/LeagueDataService.js +130 -0
- package/build/services/NhlScheduleService.js +357 -0
- package/build/services/NhlStatsService.js +297 -0
- package/build/services/RosterStore.js +233 -0
- package/package.json +12 -10
- package/scripts/preflight.mjs +67 -0
- package/scripts/smoke.mjs +92 -0
- package/.env.example +0 -8
- package/authenticate.js +0 -207
- package/build/experimental/semantic-breakout-tool.js +0 -188
- package/build/experimental/semantic-intent-parser.js +0 -222
- package/build/experimental/semantic-tool-integration.js +0 -146
- package/build/experimental/test-parser.js +0 -61
- package/build/services/YahooApiClient.js +0 -309
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🏒 Yahoo Fantasy API Client Service
|
|
3
|
-
*
|
|
4
|
-
* Handles all Yahoo Fantasy Sports API interactions including:
|
|
5
|
-
* - OAuth token management (load, save, refresh)
|
|
6
|
-
* - Authenticated API requests
|
|
7
|
-
* - Token expiration handling
|
|
8
|
-
* - Error handling and retries
|
|
9
|
-
*/
|
|
10
|
-
import fs from 'fs';
|
|
11
|
-
import https from 'https';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
16
|
-
export class YahooApiClient {
|
|
17
|
-
cachedToken = null;
|
|
18
|
-
tokenFile;
|
|
19
|
-
apiBase;
|
|
20
|
-
clientId;
|
|
21
|
-
clientSecret;
|
|
22
|
-
constructor(clientId, clientSecret, apiBase = "https://fantasysports.yahooapis.com/fantasy/v2") {
|
|
23
|
-
this.clientId = clientId;
|
|
24
|
-
this.clientSecret = clientSecret;
|
|
25
|
-
this.apiBase = apiBase;
|
|
26
|
-
this.tokenFile = path.join(__dirname, "..", "..", ".yahoo-oauth.json");
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Load OAuth token from file system
|
|
30
|
-
*/
|
|
31
|
-
loadToken() {
|
|
32
|
-
try {
|
|
33
|
-
console.error(`[DEBUG] Looking for token at: ${this.tokenFile}`);
|
|
34
|
-
if (fs.existsSync(this.tokenFile)) {
|
|
35
|
-
const tokenData = fs.readFileSync(this.tokenFile, "utf8");
|
|
36
|
-
const token = JSON.parse(tokenData);
|
|
37
|
-
console.error(`[DEBUG] Token loaded successfully`);
|
|
38
|
-
this.cachedToken = token;
|
|
39
|
-
return token;
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
console.error(`[DEBUG] Token file not found`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
console.error("[ERROR] Error loading token:", error);
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Save OAuth token to file system
|
|
52
|
-
*/
|
|
53
|
-
saveToken(token) {
|
|
54
|
-
token.expires_at = Date.now() + (token.expires_in * 1000);
|
|
55
|
-
fs.writeFileSync(this.tokenFile, JSON.stringify(token, null, 2));
|
|
56
|
-
this.cachedToken = token;
|
|
57
|
-
console.error("[DEBUG] Token saved successfully");
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Refresh expired OAuth token
|
|
61
|
-
*/
|
|
62
|
-
async refreshAccessToken() {
|
|
63
|
-
const token = this.cachedToken || this.loadToken();
|
|
64
|
-
if (!token) {
|
|
65
|
-
throw new Error("No refresh token available. Please re-authenticate.");
|
|
66
|
-
}
|
|
67
|
-
console.error("[DEBUG] Refreshing access token...");
|
|
68
|
-
const tokenData = new URLSearchParams({
|
|
69
|
-
client_id: this.clientId,
|
|
70
|
-
client_secret: this.clientSecret,
|
|
71
|
-
redirect_uri: "oob",
|
|
72
|
-
refresh_token: token.refresh_token,
|
|
73
|
-
grant_type: "refresh_token",
|
|
74
|
-
});
|
|
75
|
-
const options = {
|
|
76
|
-
hostname: "api.login.yahoo.com",
|
|
77
|
-
port: 443,
|
|
78
|
-
path: "/oauth2/get_token",
|
|
79
|
-
method: "POST",
|
|
80
|
-
headers: {
|
|
81
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
82
|
-
"Content-Length": tokenData.toString().length,
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
const req = https.request(options, (res) => {
|
|
87
|
-
let data = "";
|
|
88
|
-
res.on("data", (chunk) => {
|
|
89
|
-
data += chunk;
|
|
90
|
-
});
|
|
91
|
-
res.on("end", () => {
|
|
92
|
-
try {
|
|
93
|
-
const newToken = JSON.parse(data);
|
|
94
|
-
this.saveToken(newToken);
|
|
95
|
-
console.error("[DEBUG] Token refreshed successfully");
|
|
96
|
-
resolve(newToken.access_token);
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
reject(new Error(`Failed to parse token response: ${error}`));
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
req.on("error", (error) => {
|
|
104
|
-
reject(error);
|
|
105
|
-
});
|
|
106
|
-
req.write(tokenData.toString());
|
|
107
|
-
req.end();
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Get valid access token, refreshing if necessary
|
|
112
|
-
*/
|
|
113
|
-
async getValidAccessToken() {
|
|
114
|
-
const token = this.cachedToken || this.loadToken();
|
|
115
|
-
if (!token) {
|
|
116
|
-
throw new Error("No authentication token found! Run: node authenticate.js");
|
|
117
|
-
}
|
|
118
|
-
// Check if token is expired (with 5 minute buffer)
|
|
119
|
-
if (token.expires_at && token.expires_at < Date.now() + 300000) {
|
|
120
|
-
console.error("[DEBUG] Token expired or expiring soon, refreshing...");
|
|
121
|
-
return await this.refreshAccessToken();
|
|
122
|
-
}
|
|
123
|
-
return token.access_token;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Make authenticated request to Yahoo Fantasy API
|
|
127
|
-
*
|
|
128
|
-
* @param endpoint - API endpoint path (e.g., "/team/nhl.l.12345.t.1/roster")
|
|
129
|
-
* @param format - Response format (default: "json")
|
|
130
|
-
* @returns Parsed API response
|
|
131
|
-
*/
|
|
132
|
-
async request(endpoint, format = "json") {
|
|
133
|
-
const accessToken = await this.getValidAccessToken();
|
|
134
|
-
const url = `${this.apiBase}${endpoint}${endpoint.includes('?') ? '&' : '?'}format=${format}`;
|
|
135
|
-
console.error(`[DEBUG] API Request: ${endpoint}`);
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
const urlObj = new URL(url);
|
|
138
|
-
const options = {
|
|
139
|
-
hostname: urlObj.hostname,
|
|
140
|
-
port: 443,
|
|
141
|
-
path: urlObj.pathname + urlObj.search,
|
|
142
|
-
method: "GET",
|
|
143
|
-
headers: {
|
|
144
|
-
Authorization: `Bearer ${accessToken}`,
|
|
145
|
-
Accept: "application/json",
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
const req = https.request(options, (res) => {
|
|
149
|
-
let data = "";
|
|
150
|
-
res.on("data", (chunk) => {
|
|
151
|
-
data += chunk;
|
|
152
|
-
});
|
|
153
|
-
res.on("end", () => {
|
|
154
|
-
if (res.statusCode === 401) {
|
|
155
|
-
// Token expired, try to refresh and retry
|
|
156
|
-
this.refreshAccessToken()
|
|
157
|
-
.then(() => this.request(endpoint, format))
|
|
158
|
-
.then(resolve)
|
|
159
|
-
.catch(reject);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (res.statusCode !== 200) {
|
|
163
|
-
reject(new Error(`API returned status ${res.statusCode}: ${data}`));
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
const parsed = JSON.parse(data);
|
|
168
|
-
resolve(parsed);
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
reject(new Error(`Failed to parse API response: ${error}`));
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
req.on("error", (error) => {
|
|
176
|
-
reject(error);
|
|
177
|
-
});
|
|
178
|
-
req.end();
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Helper: Strip nhl.l. prefix from league ID if present
|
|
183
|
-
*/
|
|
184
|
-
stripLeaguePrefix(leagueId) {
|
|
185
|
-
return leagueId.replace(/^nhl\.l\./, '');
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Helper: Strip team number from full team ID (nhl.l.12345.t.6 -> 6)
|
|
189
|
-
*/
|
|
190
|
-
extractTeamNumber(teamId) {
|
|
191
|
-
const match = teamId.match(/\.t\.(\d+)$/);
|
|
192
|
-
return match ? match[1] : teamId.replace(/^.*\.t\./, '');
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Convenience method: Get team roster
|
|
196
|
-
*/
|
|
197
|
-
async getTeamRoster(leagueId, teamId) {
|
|
198
|
-
const cleanLeagueId = this.stripLeaguePrefix(leagueId);
|
|
199
|
-
const cleanTeamId = this.extractTeamNumber(teamId);
|
|
200
|
-
return this.request(`/team/nhl.l.${cleanLeagueId}.t.${cleanTeamId}/roster`);
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Convenience method: Get league standings
|
|
204
|
-
*/
|
|
205
|
-
async getLeagueStandings(leagueId) {
|
|
206
|
-
const cleanLeagueId = this.stripLeaguePrefix(leagueId);
|
|
207
|
-
return this.request(`/league/nhl.l.${cleanLeagueId}/standings`);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Convenience method: Get team matchup
|
|
211
|
-
*/
|
|
212
|
-
async getTeamMatchup(leagueId, teamId, week) {
|
|
213
|
-
const cleanLeagueId = this.stripLeaguePrefix(leagueId);
|
|
214
|
-
const cleanTeamId = this.extractTeamNumber(teamId);
|
|
215
|
-
const weekParam = week ? `;week=${week}` : '';
|
|
216
|
-
return this.request(`/team/nhl.l.${cleanLeagueId}.t.${cleanTeamId}/matchups${weekParam}`);
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Convenience method: Get league scoreboard
|
|
220
|
-
*/
|
|
221
|
-
async getLeagueScoreboard(leagueId, week) {
|
|
222
|
-
const cleanLeagueId = this.stripLeaguePrefix(leagueId);
|
|
223
|
-
const weekParam = week ? `;week=${week}` : '';
|
|
224
|
-
return this.request(`/league/nhl.l.${cleanLeagueId}/scoreboard${weekParam}`);
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Convenience method: Get team stats
|
|
228
|
-
*/
|
|
229
|
-
async getTeamStats(leagueId, teamId) {
|
|
230
|
-
const cleanLeagueId = this.stripLeaguePrefix(leagueId);
|
|
231
|
-
const cleanTeamId = this.extractTeamNumber(teamId);
|
|
232
|
-
return this.request(`/team/nhl.l.${cleanLeagueId}.t.${cleanTeamId}/stats`);
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Convenience method: Get league settings
|
|
236
|
-
*/
|
|
237
|
-
async getLeagueSettings(leagueId) {
|
|
238
|
-
return this.request(`/league/nhl.l.${leagueId}/settings`);
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Convenience method: Get players (for search, trending, etc.)
|
|
242
|
-
*/
|
|
243
|
-
async getPlayers(leagueId, queryParams = '') {
|
|
244
|
-
return this.request(`/league/nhl.l.${leagueId}/players${queryParams}`);
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Convenience method: Search players by position
|
|
248
|
-
*/
|
|
249
|
-
async searchPlayers(position, count = 25, leagueId) {
|
|
250
|
-
const league = leagueId || process.env.YAHOO_LEAGUE_ID;
|
|
251
|
-
const cleanLeagueId = this.stripLeaguePrefix(league);
|
|
252
|
-
let queryParams = `;status=A;count=${count}`;
|
|
253
|
-
if (position) {
|
|
254
|
-
queryParams += `;position=${position}`;
|
|
255
|
-
}
|
|
256
|
-
const data = await this.request(`/league/nhl.l.${cleanLeagueId}/players${queryParams}`);
|
|
257
|
-
// Parse player data
|
|
258
|
-
const playersData = data.fantasy_content.league[1].players;
|
|
259
|
-
const players = Object.keys(playersData)
|
|
260
|
-
.filter(key => key !== 'count')
|
|
261
|
-
.map(key => {
|
|
262
|
-
const playerData = playersData[key].player[0];
|
|
263
|
-
const playerId = playerData.find((item) => item.player_id)?.player_id;
|
|
264
|
-
const name = playerData.find((item) => item.name)?.name?.full;
|
|
265
|
-
const displayPosition = playerData.find((item) => item.display_position)?.display_position;
|
|
266
|
-
const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
|
|
267
|
-
const percentOwned = playerData.find((item) => item.percent_owned)?.percent_owned?.value || "0";
|
|
268
|
-
return {
|
|
269
|
-
player_id: playerId,
|
|
270
|
-
name: name,
|
|
271
|
-
position: displayPosition,
|
|
272
|
-
team: team,
|
|
273
|
-
percent_owned: parseFloat(percentOwned),
|
|
274
|
-
selected_position: []
|
|
275
|
-
};
|
|
276
|
-
});
|
|
277
|
-
return { players };
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Convenience method: Get trending players
|
|
281
|
-
*/
|
|
282
|
-
async getTrendingPlayers(trendType = 'add', count = 25, leagueId) {
|
|
283
|
-
const league = leagueId || process.env.YAHOO_LEAGUE_ID;
|
|
284
|
-
const cleanLeagueId = this.stripLeaguePrefix(league);
|
|
285
|
-
const sortParam = trendType === 'add' ? 'AR' : 'OR';
|
|
286
|
-
const data = await this.request(`/league/nhl.l.${cleanLeagueId}/players;status=A;sort=${sortParam};count=${count}`);
|
|
287
|
-
const playersData = data.fantasy_content.league[1].players;
|
|
288
|
-
const players = Object.keys(playersData)
|
|
289
|
-
.filter(key => key !== 'count')
|
|
290
|
-
.map(key => {
|
|
291
|
-
const playerData = playersData[key].player[0];
|
|
292
|
-
const playerId = playerData.find((item) => item.player_id)?.player_id;
|
|
293
|
-
const name = playerData.find((item) => item.name)?.name?.full;
|
|
294
|
-
const displayPosition = playerData.find((item) => item.display_position)?.display_position;
|
|
295
|
-
const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
|
|
296
|
-
const percentOwned = playerData.find((item) => item.percent_owned)?.percent_owned?.value || "0";
|
|
297
|
-
return {
|
|
298
|
-
player_id: playerId,
|
|
299
|
-
name: name,
|
|
300
|
-
position: displayPosition,
|
|
301
|
-
team: team,
|
|
302
|
-
percent_owned: parseFloat(percentOwned),
|
|
303
|
-
trending: trendType,
|
|
304
|
-
selected_position: []
|
|
305
|
-
};
|
|
306
|
-
});
|
|
307
|
-
return { players, trend_type: trendType };
|
|
308
|
-
}
|
|
309
|
-
}
|