bbdata-cli 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.
Files changed (35) hide show
  1. package/dist/bin/bbdata.js +128 -16
  2. package/dist/bin/bbdata.js.map +1 -1
  3. package/dist/src/index.d.ts +3 -1
  4. package/dist/src/index.js +122 -12
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/templates/queries/hitter-batted-ball.ts +66 -0
  7. package/dist/templates/queries/hitter-hot-cold-zones.ts +81 -0
  8. package/dist/templates/queries/hitter-vs-pitch-type.ts +78 -0
  9. package/dist/templates/queries/index.ts +24 -0
  10. package/dist/templates/queries/leaderboard-comparison.ts +72 -0
  11. package/dist/templates/queries/leaderboard-custom.ts +90 -0
  12. package/dist/templates/queries/matchup-pitcher-vs-hitter.ts +81 -0
  13. package/dist/templates/queries/matchup-situational.ts +68 -0
  14. package/dist/templates/queries/pitcher-arsenal.ts +89 -0
  15. package/dist/templates/queries/pitcher-handedness-splits.ts +81 -0
  16. package/dist/templates/queries/pitcher-velocity-trend.ts +73 -0
  17. package/dist/templates/queries/registry.ts +73 -0
  18. package/dist/templates/queries/trend-rolling-average.ts +86 -0
  19. package/dist/templates/queries/trend-year-over-year.ts +73 -0
  20. package/dist/templates/reports/advance-lineup.hbs +29 -0
  21. package/dist/templates/reports/advance-sp.hbs +60 -0
  22. package/dist/templates/reports/college-hitter-draft.hbs +49 -0
  23. package/dist/templates/reports/college-pitcher-draft.hbs +48 -0
  24. package/dist/templates/reports/dev-progress.hbs +29 -0
  25. package/dist/templates/reports/draft-board-card.hbs +35 -0
  26. package/dist/templates/reports/hs-prospect.hbs +48 -0
  27. package/dist/templates/reports/partials/footer.hbs +7 -0
  28. package/dist/templates/reports/partials/header.hbs +12 -0
  29. package/dist/templates/reports/post-promotion.hbs +25 -0
  30. package/dist/templates/reports/pro-hitter-eval.hbs +65 -0
  31. package/dist/templates/reports/pro-pitcher-eval.hbs +69 -0
  32. package/dist/templates/reports/registry.ts +215 -0
  33. package/dist/templates/reports/relief-pitcher-quick.hbs +29 -0
  34. package/dist/templates/reports/trade-target-onepager.hbs +45 -0
  35. package/package.json +1 -1
@@ -165,8 +165,8 @@ var SavantAdapter = class {
165
165
  }
166
166
  const params = new URLSearchParams({
167
167
  all: "true",
168
- type: "detail",
169
- ...query2.stat_type === "pitching" ? { player_type: "pitcher", pitchers_lookup: playerId ?? "" } : { player_type: "batter", batters_lookup: playerId ?? "" },
168
+ type: "details",
169
+ ...query2.stat_type === "pitching" ? { player_type: "pitcher", "pitchers_lookup[]": playerId ?? "" } : { player_type: "batter", "batters_lookup[]": playerId ?? "" },
170
170
  ...query2.start_date ? { game_date_gt: query2.start_date } : { game_date_gt: `${query2.season}-01-01` },
171
171
  ...query2.end_date ? { game_date_lt: query2.end_date } : { game_date_lt: `${query2.season}-12-31` }
172
172
  });
@@ -192,6 +192,9 @@ var SavantAdapter = class {
192
192
  cast: true,
193
193
  cast_date: false
194
194
  });
195
+ if (rawRows.length === 0 && csvText.trim().length > 50) {
196
+ log.debug("Savant returned headers but no data rows");
197
+ }
195
198
  const filteredRows = rawRows.filter(
196
199
  (row) => row.pitch_type && String(row.pitch_type).trim() !== ""
197
200
  );
@@ -231,6 +234,7 @@ var SavantAdapter = class {
231
234
 
232
235
  // src/adapters/fangraphs.ts
233
236
  var FG_LEADERS_URL = "https://www.fangraphs.com/api/leaders/major-league/data";
237
+ var stripHtml = (s) => s.replace(/<[^>]*>/g, "");
234
238
  var FanGraphsAdapter = class {
235
239
  source = "fangraphs";
236
240
  description = "FanGraphs \u2014 aggregated stats, leaderboards, WAR, wRC+, FIP";
@@ -259,7 +263,7 @@ var FanGraphsAdapter = class {
259
263
  const parsed = JSON.parse(data);
260
264
  const nameNorm = name.toLowerCase().trim();
261
265
  const players = parsed.data ?? [];
262
- const getName = (p) => String(p.PlayerName ?? p.Name ?? "").toLowerCase().trim();
266
+ const getName = (p) => stripHtml(String(p.PlayerName ?? p.Name ?? "")).toLowerCase().trim();
263
267
  let match = players.find((p) => getName(p) === nameNorm);
264
268
  if (!match) {
265
269
  const tokens = nameNorm.split(/\s+/);
@@ -275,8 +279,8 @@ var FanGraphsAdapter = class {
275
279
  return {
276
280
  mlbam_id: String(match.xMLBAMID ?? ""),
277
281
  fangraphs_id: String(match.playerid ?? ""),
278
- name: String(match.PlayerName ?? match.Name),
279
- team: String(match.Team ?? "")
282
+ name: stripHtml(String(match.PlayerName ?? match.Name)),
283
+ team: stripHtml(String(match.TeamNameAbb ?? match.TeamName ?? match.Team ?? ""))
280
284
  };
281
285
  } catch (error) {
282
286
  log.warn(`FanGraphs player resolution failed: ${error}`);
@@ -308,7 +312,7 @@ var FanGraphsAdapter = class {
308
312
  let filtered = rows;
309
313
  if (query2.player_name) {
310
314
  const nameNorm = query2.player_name.toLowerCase().trim();
311
- const getName = (r) => String(r.PlayerName ?? r.Name ?? "").toLowerCase().trim();
315
+ const getName = (r) => stripHtml(String(r.PlayerName ?? r.Name ?? "")).toLowerCase().trim();
312
316
  const exact = rows.filter((r) => getName(r) === nameNorm);
313
317
  if (exact.length > 0) {
314
318
  filtered = exact;
@@ -322,8 +326,8 @@ var FanGraphsAdapter = class {
322
326
  }
323
327
  const stats = filtered.map((row) => ({
324
328
  player_id: String(row.xMLBAMID ?? row.playerid ?? ""),
325
- player_name: String(row.PlayerName ?? row.Name ?? ""),
326
- team: String(row.Team ?? ""),
329
+ player_name: stripHtml(String(row.PlayerName ?? row.Name ?? "")),
330
+ team: stripHtml(String(row.TeamNameAbb ?? row.TeamName ?? row.Team ?? "")),
327
331
  season: query2.season,
328
332
  stat_type: query2.stat_type,
329
333
  stats: row
@@ -397,13 +401,88 @@ var BaseballReferenceAdapter = class {
397
401
  }
398
402
  };
399
403
 
404
+ // src/adapters/stdin.ts
405
+ var StdinAdapter = class {
406
+ source = "stdin";
407
+ description = "Local data from stdin (for sandboxed environments)";
408
+ data = [];
409
+ player = null;
410
+ loaded = false;
411
+ /**
412
+ * Load data from a pre-read stdin string.
413
+ * Called by the CLI before the adapter is used.
414
+ */
415
+ load(raw) {
416
+ try {
417
+ const parsed = JSON.parse(raw);
418
+ if (Array.isArray(parsed)) {
419
+ this.data = parsed;
420
+ } else if (parsed.data && Array.isArray(parsed.data)) {
421
+ this.data = parsed.data;
422
+ if (parsed.player) {
423
+ this.player = parsed.player;
424
+ }
425
+ } else {
426
+ throw new Error('Expected JSON array or { "data": [...] } object');
427
+ }
428
+ this.loaded = true;
429
+ log.info(`Stdin adapter loaded ${this.data.length} records`);
430
+ } catch (error) {
431
+ throw new Error(
432
+ `Failed to parse stdin data: ${error instanceof Error ? error.message : String(error)}`
433
+ );
434
+ }
435
+ }
436
+ supports(_query) {
437
+ return this.loaded && this.data.length > 0;
438
+ }
439
+ async fetch(query2) {
440
+ if (!this.loaded) {
441
+ throw new Error("Stdin adapter has no data \u2014 pipe JSON via stdin with --stdin flag");
442
+ }
443
+ return {
444
+ data: this.data,
445
+ source: "stdin",
446
+ cached: false,
447
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
448
+ meta: {
449
+ rowCount: this.data.length,
450
+ season: query2.season,
451
+ query: query2
452
+ }
453
+ };
454
+ }
455
+ async resolvePlayer(name) {
456
+ if (this.player && this.player.name.toLowerCase() === name.toLowerCase()) {
457
+ return this.player;
458
+ }
459
+ const firstRecord = this.data[0];
460
+ if (firstRecord) {
461
+ const id = firstRecord.pitcher_id ?? firstRecord.player_id ?? "";
462
+ const recordName = firstRecord.pitcher_name ?? firstRecord.player_name ?? name;
463
+ if (id) {
464
+ return {
465
+ mlbam_id: id,
466
+ name: recordName
467
+ };
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+ };
473
+
400
474
  // src/adapters/index.ts
475
+ var stdinAdapter = new StdinAdapter();
401
476
  var adapters = {
402
477
  "mlb-stats-api": new MlbStatsApiAdapter(),
403
478
  "savant": new SavantAdapter(),
404
479
  "fangraphs": new FanGraphsAdapter(),
405
- "baseball-reference": new BaseballReferenceAdapter()
480
+ "baseball-reference": new BaseballReferenceAdapter(),
481
+ "stdin": stdinAdapter
406
482
  };
483
+ function getStdinAdapter() {
484
+ return stdinAdapter;
485
+ }
407
486
  function resolveAdapters(preferred) {
408
487
  return preferred.map((source) => adapters[source]).filter(Boolean);
409
488
  }
@@ -574,6 +653,20 @@ function getTemplatesDir() {
574
653
  return dir;
575
654
  }
576
655
 
656
+ // src/utils/stdin.ts
657
+ function readStdin() {
658
+ return new Promise((resolve, reject) => {
659
+ if (process.stdin.isTTY) {
660
+ reject(new Error(`--stdin flag requires piped input. Usage: echo '{"data":[...]}' | bbdata query ... --stdin`));
661
+ return;
662
+ }
663
+ const chunks = [];
664
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
665
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
666
+ process.stdin.on("error", reject);
667
+ });
668
+ }
669
+
577
670
  // src/templates/queries/registry.ts
578
671
  var templates = /* @__PURE__ */ new Map();
579
672
  function registerTemplate(template13) {
@@ -1174,11 +1267,12 @@ var template9 = {
1174
1267
  "bbdata query leaderboard-custom --stat ERA --min-ip 100 --top 10 --format table"
1175
1268
  ],
1176
1269
  buildQuery(params) {
1270
+ const pitchingStats = ["era", "fip", "xfip", "siera", "whip", "k/9", "bb/9", "hr/9", "ip", "w", "sv", "hld"];
1271
+ const isPitching = pitchingStats.includes((params.stat ?? "").toLowerCase());
1177
1272
  return {
1178
1273
  season: params.season ?? (/* @__PURE__ */ new Date()).getFullYear(),
1179
1274
  team: params.team,
1180
- stat_type: "batting",
1181
- // Will be overridden based on stat
1275
+ stat_type: isPitching ? "pitching" : "batting",
1182
1276
  min_pa: params.minPa,
1183
1277
  min_ip: params.minIp
1184
1278
  };
@@ -1424,6 +1518,12 @@ registerTemplate(template12);
1424
1518
 
1425
1519
  // src/commands/query.ts
1426
1520
  async function query(options) {
1521
+ if (options.stdin) {
1522
+ const raw = await readStdin();
1523
+ const adapter = getStdinAdapter();
1524
+ adapter.load(raw);
1525
+ options.source = "stdin";
1526
+ }
1427
1527
  const config = getConfig();
1428
1528
  const outputFormat = options.format ?? config.defaultFormat;
1429
1529
  const template13 = getTemplate(options.template);
@@ -1464,6 +1564,10 @@ ${available}`);
1464
1564
  });
1465
1565
  const rows = template13.transform(adapterResult.data, params);
1466
1566
  const columns = template13.columns(params);
1567
+ if (rows.length === 0) {
1568
+ log.debug(`${adapter.source} returned 0 rows. Trying next source...`);
1569
+ continue;
1570
+ }
1467
1571
  result = {
1468
1572
  rows,
1469
1573
  columns,
@@ -1503,7 +1607,7 @@ ${available}`);
1503
1607
  };
1504
1608
  }
1505
1609
  function registerQueryCommand(program2) {
1506
- const cmd = program2.command("query [template]").description("Query baseball data using pre-built templates").option("-p, --player <name>", "Player name").option("--players <names>", "Comma-separated player names (for matchups/comparisons)").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-f, --format <fmt>", "Output: json, table, csv, markdown", "json").option("--source <src>", "Force a data source: savant, fangraphs, mlb-stats-api").option("--stat <stat>", "Stat to query (for leaderboards)").option("--pitch-type <type>", "Filter by pitch type (e.g., FF, SL)").option("--min-pa <n>", "Minimum plate appearances", parseInt).option("--min-ip <n>", "Minimum innings pitched", parseInt).option("--top <n>", "Number of results for leaderboards", parseInt).option("--seasons <range>", "Season range (e.g., 2023-2025)").option("--no-cache", "Bypass cache").addHelpText("after", `
1610
+ const cmd = program2.command("query [template]").description("Query baseball data using pre-built templates").option("-p, --player <name>", "Player name").option("--players <names>", "Comma-separated player names (for matchups/comparisons)").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-f, --format <fmt>", "Output: json, table, csv, markdown", "json").option("--source <src>", "Force a data source: savant, fangraphs, mlb-stats-api").option("--stat <stat>", "Stat to query (for leaderboards)").option("--pitch-type <type>", "Filter by pitch type (e.g., FF, SL)").option("--min-pa <n>", "Minimum plate appearances", parseInt).option("--min-ip <n>", "Minimum innings pitched", parseInt).option("--top <n>", "Number of results for leaderboards", parseInt).option("--seasons <range>", "Season range (e.g., 2023-2025)").option("--no-cache", "Bypass cache").option("--stdin", "Read pre-fetched JSON data from stdin instead of fetching from APIs").addHelpText("after", `
1507
1611
  Examples:
1508
1612
  bbdata query pitcher-arsenal --player "Corbin Burnes" --season 2025
1509
1613
  bbdata query hitter-batted-ball --player "Aaron Judge" --format table
@@ -1542,7 +1646,8 @@ Available templates:
1542
1646
  seasons: opts.seasons,
1543
1647
  format: opts.format,
1544
1648
  source: opts.source,
1545
- cache: opts.cache
1649
+ cache: opts.cache,
1650
+ stdin: opts.stdin
1546
1651
  });
1547
1652
  log.data(result.formatted);
1548
1653
  } catch (error) {
@@ -1807,6 +1912,11 @@ ${sections}
1807
1912
  `;
1808
1913
  }
1809
1914
  async function report(options) {
1915
+ if (options.stdin) {
1916
+ const raw = await readStdin();
1917
+ const adapter = getStdinAdapter();
1918
+ adapter.load(raw);
1919
+ }
1810
1920
  const config = getConfig();
1811
1921
  const audience = options.audience ?? config.defaultAudience;
1812
1922
  const template13 = getReportTemplate(options.template);
@@ -1826,7 +1936,8 @@ ${available}`);
1826
1936
  player: options.player,
1827
1937
  team: options.team,
1828
1938
  season,
1829
- format: "json"
1939
+ format: "json",
1940
+ ...options.stdin ? { source: "stdin" } : {}
1830
1941
  });
1831
1942
  dataResults[req.queryTemplate] = result.data;
1832
1943
  if (!dataSources.includes(result.meta.source)) {
@@ -1893,7 +2004,7 @@ function validateReport(content, requiredSections) {
1893
2004
  };
1894
2005
  }
1895
2006
  function registerReportCommand(program2) {
1896
- program2.command("report [template]").description("Generate scouting reports using pre-built templates").option("-p, --player <name>", "Player name").option("-t, --team <code>", "Team abbreviation").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-a, --audience <role>", "Target audience: coach, gm, scout, analyst").option("-f, --format <fmt>", "Output: markdown, json", "markdown").option("--validate", "Run validation checklist on the report").addHelpText("after", `
2007
+ program2.command("report [template]").description("Generate scouting reports using pre-built templates").option("-p, --player <name>", "Player name").option("-t, --team <code>", "Team abbreviation").option("-s, --season <year>", "Season year", String((/* @__PURE__ */ new Date()).getFullYear())).option("-a, --audience <role>", "Target audience: coach, gm, scout, analyst").option("-f, --format <fmt>", "Output: markdown, json", "markdown").option("--validate", "Run validation checklist on the report").option("--stdin", "Read pre-fetched JSON data from stdin instead of fetching from APIs").addHelpText("after", `
1897
2008
  Examples:
1898
2009
  bbdata report pro-pitcher-eval --player "Corbin Burnes"
1899
2010
  bbdata report advance-sp --player "Gerrit Cole" --audience coach --validate
@@ -1924,7 +2035,8 @@ Available templates:
1924
2035
  season: opts.season ? parseInt(opts.season) : void 0,
1925
2036
  audience: opts.audience,
1926
2037
  format: opts.format,
1927
- validate: opts.validate
2038
+ validate: opts.validate,
2039
+ stdin: opts.stdin
1928
2040
  });
1929
2041
  log.data(result.formatted);
1930
2042
  if (result.validation && !result.validation.passed) {