@pipeworx/mcp-cricsheet 0.1.0 → 0.2.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/src/store.ts DELETED
@@ -1,372 +0,0 @@
1
- /**
2
- * Reads for cricsheet (fleet #2975).
3
- *
4
- * Data: Cricsheet (cricsheet.org) -- per-match ball-by-ball JSON files for
5
- * Test, ODI and T20 International cricket plus the Indian Premier League
6
- * (ODC Attribution License, ODC-BY). Cricsheet publishes bulk files only, so
7
- * this pack builds a copy (scripts/cricsheet/load-data.mjs) into a D1
8
- * database the gateway hands it as `_d1` (pack-manifest "d1Binding":
9
- * "CRICSHEET_DB"). Each match file is parsed ball-by-ball IN THE LOADER and
10
- * collapsed to match / innings / batting-card / bowling-card rows --
11
- * individual deliveries are never stored, per CLAUDE.md "prefer the
12
- * smallest store that answers the tool questions."
13
- *
14
- * Design note: this file + index.ts.
15
- */
16
-
17
- /** The slice of Cloudflare's D1Database this pack uses, declared here so the
18
- * pack needs no workers-types and a test can pass a node:sqlite stub. */
19
- export interface D1Like {
20
- prepare(sql: string): D1StmtLike;
21
- }
22
- export interface D1StmtLike {
23
- bind(...values: unknown[]): D1StmtLike;
24
- all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;
25
- first<T = Record<string, unknown>>(): Promise<T | null>;
26
- }
27
-
28
- export class StoreUnavailableError extends Error {
29
- constructor() {
30
- super(
31
- 'cricsheet: the match store is not bound in this runtime (no _d1). These tools only answer through the Pipeworx gateway; a standalone install has no data to read.',
32
- );
33
- this.name = 'StoreUnavailableError';
34
- }
35
- }
36
- export function requireDb(d1: unknown): D1Like {
37
- if (!d1 || typeof (d1 as D1Like).prepare !== 'function') throw new StoreUnavailableError();
38
- return d1 as D1Like;
39
- }
40
-
41
- export const FORMATS = ['Test', 'ODI', 'T20I', 'IPL'] as const;
42
- export type Format = (typeof FORMATS)[number];
43
-
44
- export function resolveFormat(input: unknown): Format | null {
45
- if (input == null || input === '') return null;
46
- const s = String(input).trim().toUpperCase();
47
- const map: Record<string, Format> = {
48
- TEST: 'Test', TESTS: 'Test',
49
- ODI: 'ODI', ODIS: 'ODI', 'ONE DAY INTERNATIONAL': 'ODI', 'ONE-DAY INTERNATIONAL': 'ODI',
50
- T20I: 'T20I', T20IS: 'T20I', T20: 'T20I', 'T20 INTERNATIONAL': 'T20I', 'T20 INTERNATIONALS': 'T20I',
51
- IPL: 'IPL', 'INDIAN PREMIER LEAGUE': 'IPL',
52
- };
53
- return map[s] ?? null;
54
- }
55
-
56
- export async function dataAsOf(db: D1Like): Promise<string | null> {
57
- const r = await db.prepare(`SELECT value FROM cricsheet_meta WHERE key = 'loaded_at'`).first<{ value: string }>();
58
- return r?.value ?? null;
59
- }
60
-
61
- export interface MatchRow {
62
- match_id: number;
63
- format: string;
64
- gender: string;
65
- team1: string;
66
- team2: string;
67
- match_date: string;
68
- season: string | null;
69
- venue: string | null;
70
- city: string | null;
71
- event_name: string | null;
72
- toss_winner: string | null;
73
- toss_decision: string | null;
74
- outcome_winner: string | null;
75
- outcome_by_runs: number | null;
76
- outcome_by_wickets: number | null;
77
- outcome_by_innings: number | null;
78
- outcome_method: string | null;
79
- result: string | null;
80
- player_of_match: string | null;
81
- }
82
-
83
- export async function getMatch(db: D1Like, matchId: number): Promise<MatchRow | null> {
84
- return db
85
- .prepare(
86
- `SELECT match_id, format, gender, team1, team2, match_date, season, venue, city, event_name,
87
- toss_winner, toss_decision, outcome_winner, outcome_by_runs, outcome_by_wickets,
88
- outcome_by_innings, outcome_method, result, player_of_match
89
- FROM matches WHERE match_id = ?`,
90
- )
91
- .bind(matchId)
92
- .first<MatchRow>();
93
- }
94
-
95
- export interface InningsRow {
96
- innings_num: number;
97
- batting_team: string;
98
- total_runs: number;
99
- total_wickets: number;
100
- legal_balls: number;
101
- }
102
- export async function getInnings(db: D1Like, matchId: number): Promise<InningsRow[]> {
103
- const r = await db
104
- .prepare(`SELECT innings_num, batting_team, total_runs, total_wickets, legal_balls FROM innings WHERE match_id = ? ORDER BY innings_num`)
105
- .bind(matchId)
106
- .all<InningsRow>();
107
- return r.results;
108
- }
109
-
110
- export interface BattingCardRow {
111
- innings_num: number;
112
- position: number;
113
- player: string;
114
- runs: number;
115
- balls: number;
116
- fours: number;
117
- sixes: number;
118
- not_out: number;
119
- dismissal_kind: string | null;
120
- dismissal_bowler: string | null;
121
- }
122
- export async function getBattingCards(db: D1Like, matchId: number): Promise<BattingCardRow[]> {
123
- const r = await db
124
- .prepare(
125
- `SELECT innings_num, position, player, runs, balls, fours, sixes, not_out, dismissal_kind, dismissal_bowler
126
- FROM batting_cards WHERE match_id = ? ORDER BY innings_num, position`,
127
- )
128
- .bind(matchId)
129
- .all<BattingCardRow>();
130
- return r.results;
131
- }
132
-
133
- export interface BowlingCardRow {
134
- innings_num: number;
135
- order_num: number;
136
- player: string;
137
- balls: number;
138
- runs_conceded: number;
139
- wickets: number;
140
- }
141
- export async function getBowlingCards(db: D1Like, matchId: number): Promise<BowlingCardRow[]> {
142
- const r = await db
143
- .prepare(
144
- `SELECT innings_num, order_num, player, balls, runs_conceded, wickets
145
- FROM bowling_cards WHERE match_id = ? ORDER BY innings_num, order_num`,
146
- )
147
- .bind(matchId)
148
- .all<BowlingCardRow>();
149
- return r.results;
150
- }
151
-
152
- /** Resolve a free-text player query against the small `players` table
153
- * (one row per distinct name string Cricsheet uses). Cricsheet names are
154
- * short forms ("V Kohli", "SR Watson") -- matching is case-insensitive
155
- * substring, ranked by how many matches that name appears in, so a common
156
- * surname like "Kohli" resolves unambiguously while a genuinely ambiguous
157
- * query (e.g. a shared surname) comes back as a candidate list rather than
158
- * picking one silently. */
159
- export interface PlayerCandidate {
160
- player: string;
161
- matches: number;
162
- }
163
- export async function resolvePlayer(db: D1Like, query: string): Promise<{ resolved: string | null; candidates: PlayerCandidate[] }> {
164
- const exact = await db
165
- .prepare(`SELECT player, matches FROM players WHERE player = ? COLLATE NOCASE`)
166
- .bind(query)
167
- .first<PlayerCandidate>();
168
- if (exact) return { resolved: exact.player, candidates: [exact] };
169
-
170
- const r = await db
171
- .prepare(`SELECT player, matches FROM players WHERE player LIKE ? COLLATE NOCASE ORDER BY matches DESC LIMIT 20`)
172
- .bind(`%${query}%`)
173
- .all<PlayerCandidate>();
174
- if (r.results.length === 1) return { resolved: r.results[0].player, candidates: r.results };
175
- return { resolved: null, candidates: r.results };
176
- }
177
-
178
- export interface BattingAgg {
179
- innings: number;
180
- runs: number;
181
- balls: number;
182
- outs: number;
183
- not_outs: number;
184
- fours: number;
185
- sixes: number;
186
- high_score: number | null;
187
- hundreds: number;
188
- fifties: number;
189
- average: number | null;
190
- strike_rate: number | null;
191
- }
192
- export interface BowlingAgg {
193
- innings: number;
194
- balls: number;
195
- runs_conceded: number;
196
- wickets: number;
197
- best_wickets: number | null;
198
- average: number | null;
199
- economy: number | null;
200
- }
201
-
202
- export async function battingAggregate(db: D1Like, player: string, format: Format | null): Promise<BattingAgg> {
203
- const where = format ? 'player = ? AND format = ?' : 'player = ?';
204
- const params = format ? [player, format] : [player];
205
- const row = await db
206
- .prepare(
207
- `SELECT COUNT(*) AS innings, COALESCE(SUM(runs),0) AS runs, COALESCE(SUM(balls),0) AS balls,
208
- COALESCE(SUM(CASE WHEN not_out = 0 THEN 1 ELSE 0 END),0) AS outs,
209
- COALESCE(SUM(CASE WHEN not_out = 1 THEN 1 ELSE 0 END),0) AS not_outs,
210
- COALESCE(SUM(fours),0) AS fours, COALESCE(SUM(sixes),0) AS sixes,
211
- MAX(runs) AS high_score,
212
- COALESCE(SUM(CASE WHEN runs >= 100 THEN 1 ELSE 0 END),0) AS hundreds,
213
- COALESCE(SUM(CASE WHEN runs >= 50 AND runs < 100 THEN 1 ELSE 0 END),0) AS fifties
214
- FROM batting_cards WHERE ${where}`,
215
- )
216
- .bind(...params)
217
- .first<{ innings: number; runs: number; balls: number; outs: number; not_outs: number; fours: number; sixes: number; high_score: number | null; hundreds: number; fifties: number }>();
218
- const innings = row?.innings ?? 0;
219
- const runs = row?.runs ?? 0;
220
- const balls = row?.balls ?? 0;
221
- const outs = row?.outs ?? 0;
222
- return {
223
- innings,
224
- runs,
225
- balls,
226
- outs,
227
- not_outs: row?.not_outs ?? 0,
228
- fours: row?.fours ?? 0,
229
- sixes: row?.sixes ?? 0,
230
- high_score: row?.high_score ?? null,
231
- hundreds: row?.hundreds ?? 0,
232
- fifties: row?.fifties ?? 0,
233
- average: outs > 0 ? round2(runs / outs) : null,
234
- strike_rate: balls > 0 ? round2((runs / balls) * 100) : null,
235
- };
236
- }
237
-
238
- export async function bowlingAggregate(db: D1Like, player: string, format: Format | null): Promise<BowlingAgg> {
239
- const where = format ? 'player = ? AND format = ?' : 'player = ?';
240
- const params = format ? [player, format] : [player];
241
- const row = await db
242
- .prepare(
243
- `SELECT COUNT(*) AS innings, COALESCE(SUM(balls),0) AS balls, COALESCE(SUM(runs_conceded),0) AS runs_conceded,
244
- COALESCE(SUM(wickets),0) AS wickets, MAX(wickets) AS best_wickets
245
- FROM bowling_cards WHERE ${where}`,
246
- )
247
- .bind(...params)
248
- .first<{ innings: number; balls: number; runs_conceded: number; wickets: number; best_wickets: number | null }>();
249
- const innings = row?.innings ?? 0;
250
- const balls = row?.balls ?? 0;
251
- const runs_conceded = row?.runs_conceded ?? 0;
252
- const wickets = row?.wickets ?? 0;
253
- return {
254
- innings,
255
- balls,
256
- runs_conceded,
257
- wickets,
258
- best_wickets: row?.best_wickets ?? null,
259
- average: wickets > 0 ? round2(runs_conceded / wickets) : null,
260
- economy: balls > 0 ? round2(runs_conceded / (balls / 6)) : null,
261
- };
262
- }
263
-
264
- function round2(n: number): number {
265
- return Math.round(n * 100) / 100;
266
- }
267
-
268
- export interface HeadToHeadFilter {
269
- team_a: string;
270
- team_b: string;
271
- format: Format | null;
272
- limit: number;
273
- offset: number;
274
- }
275
- export async function headToHeadMatches(db: D1Like, f: HeadToHeadFilter): Promise<MatchRow[]> {
276
- const where = ['((team1 = ? COLLATE NOCASE AND team2 = ? COLLATE NOCASE) OR (team1 = ? COLLATE NOCASE AND team2 = ? COLLATE NOCASE))'];
277
- const params: unknown[] = [f.team_a, f.team_b, f.team_b, f.team_a];
278
- if (f.format) {
279
- where.push('format = ?');
280
- params.push(f.format);
281
- }
282
- const sql = `SELECT match_id, format, gender, team1, team2, match_date, season, venue, city, event_name,
283
- toss_winner, toss_decision, outcome_winner, outcome_by_runs, outcome_by_wickets,
284
- outcome_by_innings, outcome_method, result, player_of_match
285
- FROM matches WHERE ${where.join(' AND ')} ORDER BY match_date DESC LIMIT ? OFFSET ?`;
286
- params.push(f.limit, f.offset);
287
- const r = await db.prepare(sql).bind(...params).all<MatchRow>();
288
- return r.results;
289
- }
290
- export async function headToHeadCount(db: D1Like, f: Omit<HeadToHeadFilter, 'limit' | 'offset'>): Promise<number> {
291
- const where = ['((team1 = ? COLLATE NOCASE AND team2 = ? COLLATE NOCASE) OR (team1 = ? COLLATE NOCASE AND team2 = ? COLLATE NOCASE))'];
292
- const params: unknown[] = [f.team_a, f.team_b, f.team_b, f.team_a];
293
- if (f.format) {
294
- where.push('format = ?');
295
- params.push(f.format);
296
- }
297
- const r = await db.prepare(`SELECT COUNT(*) AS n FROM matches WHERE ${where.join(' AND ')}`).bind(...params).first<{ n: number }>();
298
- return r?.n ?? 0;
299
- }
300
-
301
- export interface FindMatchesFilter {
302
- team?: string;
303
- format?: Format | null;
304
- season?: string;
305
- venue?: string;
306
- player?: string;
307
- limit: number;
308
- offset: number;
309
- }
310
- export async function findMatches(db: D1Like, f: FindMatchesFilter): Promise<MatchRow[]> {
311
- const where: string[] = [];
312
- const params: unknown[] = [];
313
- if (f.team) {
314
- where.push('(team1 = ? COLLATE NOCASE OR team2 = ? COLLATE NOCASE)');
315
- params.push(f.team, f.team);
316
- }
317
- if (f.format) {
318
- where.push('format = ?');
319
- params.push(f.format);
320
- }
321
- if (f.season) {
322
- where.push('season = ? COLLATE NOCASE');
323
- params.push(f.season);
324
- }
325
- if (f.venue) {
326
- where.push('venue LIKE ? COLLATE NOCASE');
327
- params.push(`%${f.venue}%`);
328
- }
329
- if (f.player) {
330
- where.push(
331
- 'match_id IN (SELECT match_id FROM batting_cards WHERE player = ? COLLATE NOCASE UNION SELECT match_id FROM bowling_cards WHERE player = ? COLLATE NOCASE)',
332
- );
333
- params.push(f.player, f.player);
334
- }
335
- const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
336
- const sql = `SELECT match_id, format, gender, team1, team2, match_date, season, venue, city, event_name,
337
- toss_winner, toss_decision, outcome_winner, outcome_by_runs, outcome_by_wickets,
338
- outcome_by_innings, outcome_method, result, player_of_match
339
- FROM matches ${whereSql} ORDER BY match_date DESC LIMIT ? OFFSET ?`;
340
- params.push(f.limit, f.offset);
341
- const r = await db.prepare(sql).bind(...params).all<MatchRow>();
342
- return r.results;
343
- }
344
- export async function findMatchesCount(db: D1Like, f: Omit<FindMatchesFilter, 'limit' | 'offset'>): Promise<number> {
345
- const where: string[] = [];
346
- const params: unknown[] = [];
347
- if (f.team) {
348
- where.push('(team1 = ? COLLATE NOCASE OR team2 = ? COLLATE NOCASE)');
349
- params.push(f.team, f.team);
350
- }
351
- if (f.format) {
352
- where.push('format = ?');
353
- params.push(f.format);
354
- }
355
- if (f.season) {
356
- where.push('season = ? COLLATE NOCASE');
357
- params.push(f.season);
358
- }
359
- if (f.venue) {
360
- where.push('venue LIKE ? COLLATE NOCASE');
361
- params.push(`%${f.venue}%`);
362
- }
363
- if (f.player) {
364
- where.push(
365
- 'match_id IN (SELECT match_id FROM batting_cards WHERE player = ? COLLATE NOCASE UNION SELECT match_id FROM bowling_cards WHERE player = ? COLLATE NOCASE)',
366
- );
367
- params.push(f.player, f.player);
368
- }
369
- const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
370
- const r = await db.prepare(`SELECT COUNT(*) AS n FROM matches ${whereSql}`).bind(...params).first<{ n: number }>();
371
- return r?.n ?? 0;
372
- }
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022"],
7
- "types": ["@cloudflare/workers-types"],
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "resolveJsonModule": true,
12
- "outDir": "dist",
13
- "rootDir": "src",
14
- "declaration": true
15
- },
16
- "include": ["src"],
17
- "exclude": ["src/server.ts"]
18
- }