@claudinho/core 0.8.17 → 0.8.19

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/dist/index.d.ts CHANGED
@@ -183,6 +183,54 @@ declare function isValidTimeZone(tz: string | undefined | null): boolean;
183
183
  */
184
184
  declare function isValidDate(s: string | undefined | null): boolean;
185
185
 
186
+ /**
187
+ * Feed-string sanitizer — the chokepoint between untrusted provider data and
188
+ * every output surface (terminal, statusline, share cards, and the Claude Code
189
+ * hook, whose stdout lands in the model's context). Strips control characters
190
+ * (C0 incl. ESC, DEL, C1) so a compromised feed can't inject ANSI escapes or
191
+ * multi-line text, and caps length so one field can't flood a surface.
192
+ *
193
+ * Applied at the ESPN adapter boundary (toTeam / mapEspnEvent) and mirrored on
194
+ * the statusline's cache reads (defense against a poisoned cache file).
195
+ */
196
+
197
+ /** Default per-field cap — generous for any real team/venue name. */
198
+ declare const FEED_TEXT_MAX = 100;
199
+ /**
200
+ * Strip C0/C1 control characters (including ESC) and cap at `max` code points.
201
+ * Whitespace controls (tab/newline/CR) become a single space so words a hostile
202
+ * feed split across lines don't fuse together. Total: never throws.
203
+ */
204
+ declare function sanitizeFeedText(value: string, max?: number): string;
205
+ /**
206
+ * Sanitized, display-safe copy of a Match. Used on cache reads (the
207
+ * statusline/hook render straight from the cache file), so it must be total:
208
+ * a malformed entry yields empty strings, never a throw. Beyond the string
209
+ * fields, the RENDERED numeric fields (score, shootout, minute) are dropped
210
+ * unless they are real finite numbers — poisoned values degrade to "vs" /
211
+ * "LIVE", never to injected text. Shootout never survives without its score
212
+ * (the adapter-level invariant, re-enforced here).
213
+ */
214
+ declare function sanitizeMatchStrings(m: Match): Match;
215
+
216
+ /**
217
+ * Display-width helpers for monospace/plain-text alignment.
218
+ *
219
+ * `String.prototype.padEnd` counts UTF-16 units, but emoji flags occupy 2
220
+ * terminal columns regardless of how many units encode them: a regional-
221
+ * indicator flag (🇲🇽) is 4 units, while a tag-sequence flag (England 🏴󠁧󠁢󠁥󠁮󠁧󠁿) is
222
+ * 14 — so unit-counted padding pushed England/Scotland rows ~10 columns out of
223
+ * line. These helpers count grapheme clusters, with pictographic clusters
224
+ * (flags included) as 2 columns, matching how terminals render them.
225
+ */
226
+ /** Terminal display width of a string (grapheme clusters; emoji count as 2). */
227
+ declare function displayWidth(s: string): number;
228
+ /**
229
+ * Pad with trailing spaces to `width` DISPLAY columns (never truncates — a
230
+ * too-long value overflows its column rather than being cut mid-name).
231
+ */
232
+ declare function padVisible(s: string, width: number): string;
233
+
186
234
  /** Outcome (home perspective) from a final/looking scoreline. */
187
235
  declare function outcomeFromScore(home: number, away: number): Outcome;
188
236
  /** Is the match currently in play (including halftime)? */
@@ -355,6 +403,30 @@ declare function currentOrNextFixtureForTeam(code: string, opts?: {
355
403
  /** Sorted list of distinct group letters present in the schedule. */
356
404
  declare function groups(fixtures?: Match[]): string[];
357
405
 
406
+ /** A roster team plus its group letter (A–L). */
407
+ interface TeamInfo extends Team {
408
+ group?: string;
409
+ }
410
+ /**
411
+ * The 48-team roster from the bundled group-stage fixtures, sorted by name.
412
+ * Placeholder/knockout slots (non-3-letter code or 🏳️) are skipped.
413
+ */
414
+ declare function allTeams(fixtures?: Match[]): TeamInfo[];
415
+ interface TeamLookup {
416
+ query: string;
417
+ /** The single confident match, or null when the query is ambiguous or unknown. */
418
+ team: TeamInfo | null;
419
+ /** All candidate teams, best-first (1+ whenever anything matched). */
420
+ matches: TeamInfo[];
421
+ }
422
+ /**
423
+ * Resolve a free-text team name or code to a roster team. Precedence: exact code,
424
+ * exact name, alias, then prefix/substring fuzzy on the name (fuzzy only for
425
+ * queries of 3+ letters, so a 1–2 char query can't substring-match noise). A
426
+ * unique fuzzy hit resolves; multiple hits return as candidates with `team: null`.
427
+ */
428
+ declare function lookupTeam(query: string, fixtures?: Match[]): TeamLookup;
429
+
358
430
  /** One row of a group table. */
359
431
  interface StandingRow {
360
432
  team: Team;
@@ -1152,4 +1224,4 @@ declare function formatShareBracket(input: ShareBracketInput, options?: ShareBra
1152
1224
  /** Compact one-line-per-match bracket for narrow share contexts. */
1153
1225
  declare function formatBracketCompactLine(mv: BracketMatchView, opts?: BracketFormatOpts): string;
1154
1226
 
1155
- export { BRACKET_STAGE_ORDER, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, allFixtures, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, loadBracketTopology, localDate, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, scoreline, stageLabel, stageLabelI18n, t };
1227
+ export { BRACKET_STAGE_ORDER, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FEED_TEXT_MAX, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, displayWidth, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, sanitizeFeedText, sanitizeMatchStrings, scoreline, stageLabel, stageLabelI18n, t };
package/dist/index.js CHANGED
@@ -502,6 +502,67 @@ function shiftUtcDate(dateISO, days) {
502
502
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
503
503
  }
504
504
 
505
+ // src/sanitize.ts
506
+ var FEED_TEXT_MAX = 100;
507
+ function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
508
+ let out = "";
509
+ let count = 0;
510
+ for (const ch of String(value)) {
511
+ const cp = ch.codePointAt(0) ?? 0;
512
+ const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
513
+ if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
514
+ if (count >= max) break;
515
+ out += isWhitespaceControl ? " " : ch;
516
+ count++;
517
+ }
518
+ return out;
519
+ }
520
+ function sanitizeTeam(t2) {
521
+ return {
522
+ ...t2 ?? {},
523
+ code: sanitizeFeedText(t2?.code ?? ""),
524
+ name: sanitizeFeedText(t2?.name ?? ""),
525
+ flag: sanitizeFeedText(t2?.flag ?? "")
526
+ };
527
+ }
528
+ function finiteOrUndefined(v) {
529
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
530
+ }
531
+ function sanitizeScorePair(v) {
532
+ const home = finiteOrUndefined(v?.home);
533
+ const away = finiteOrUndefined(v?.away);
534
+ return home !== void 0 && away !== void 0 ? { home, away } : void 0;
535
+ }
536
+ function sanitizeMatchStrings(m) {
537
+ const score = sanitizeScorePair(m.score);
538
+ return {
539
+ ...m,
540
+ venue: sanitizeFeedText(m.venue ?? ""),
541
+ city: m.city == null ? m.city : sanitizeFeedText(m.city),
542
+ country: m.country == null ? m.country : sanitizeFeedText(m.country),
543
+ home: sanitizeTeam(m.home),
544
+ away: sanitizeTeam(m.away),
545
+ score,
546
+ shootout: score ? sanitizeScorePair(m.shootout) : void 0,
547
+ minute: finiteOrUndefined(m.minute)
548
+ };
549
+ }
550
+
551
+ // src/text.ts
552
+ var segmenter = new Intl.Segmenter();
553
+ var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
554
+ function displayWidth(s) {
555
+ let w = 0;
556
+ for (const { segment } of segmenter.segment(s)) {
557
+ w += WIDE_CLUSTER.test(segment) ? 2 : 1;
558
+ }
559
+ return w;
560
+ }
561
+ function padVisible(s, width) {
562
+ const w = displayWidth(s);
563
+ return w >= width ? s : s + " ".repeat(width - w);
564
+ }
565
+
505
566
  // src/normalize.ts
506
567
  function outcomeFromScore(home, away) {
507
568
  if (home > away) return "H";
@@ -2900,6 +2961,62 @@ function groups(fixtures = SCHEDULE) {
2900
2961
  return [...set].sort();
2901
2962
  }
2902
2963
 
2964
+ // src/teams.ts
2965
+ function norm2(s) {
2966
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z]/g, "");
2967
+ }
2968
+ function allTeams(fixtures = allFixtures()) {
2969
+ const seen = /* @__PURE__ */ new Map();
2970
+ for (const m of fixtures) {
2971
+ if (m.stage !== "GROUP") continue;
2972
+ for (const t2 of [m.home, m.away]) {
2973
+ if (!/^[A-Z]{3}$/.test(t2.code) || t2.flag === "\u{1F3F3}\uFE0F") continue;
2974
+ if (!seen.has(t2.code)) seen.set(t2.code, { ...t2, group: m.group ?? void 0 });
2975
+ }
2976
+ }
2977
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
2978
+ }
2979
+ var TEAM_ALIASES = {
2980
+ turkey: "TUR",
2981
+ holland: "NED",
2982
+ korea: "KOR",
2983
+ skorea: "KOR",
2984
+ korearepublic: "KOR",
2985
+ republicofkorea: "KOR",
2986
+ czechrepublic: "CZE",
2987
+ congo: "COD",
2988
+ drcongo: "COD",
2989
+ drc: "COD",
2990
+ democraticrepublicofcongo: "COD",
2991
+ democraticrepublicofthecongo: "COD",
2992
+ cotedivoire: "CIV",
2993
+ caboverde: "CPV",
2994
+ bosnia: "BIH",
2995
+ bosniaandherzegovina: "BIH",
2996
+ us: "USA",
2997
+ america: "USA",
2998
+ unitedstatesofamerica: "USA"
2999
+ };
3000
+ function lookupTeam(query, fixtures = allFixtures()) {
3001
+ const roster = allTeams(fixtures);
3002
+ const q = norm2(query);
3003
+ if (!q) return { query, team: null, matches: [] };
3004
+ const byCode = roster.find((t2) => norm2(t2.code) === q);
3005
+ if (byCode) return { query, team: byCode, matches: [byCode] };
3006
+ const byName = roster.find((t2) => norm2(t2.name) === q);
3007
+ if (byName) return { query, team: byName, matches: [byName] };
3008
+ const aliasCode = TEAM_ALIASES[q];
3009
+ if (aliasCode) {
3010
+ const t2 = roster.find((r) => r.code === aliasCode);
3011
+ if (t2) return { query, team: t2, matches: [t2] };
3012
+ }
3013
+ if (q.length < 3) return { query, team: null, matches: [] };
3014
+ const prefix = roster.filter((t2) => norm2(t2.name).startsWith(q));
3015
+ const substr = roster.filter((t2) => norm2(t2.name).includes(q) && !prefix.includes(t2));
3016
+ const matches = [...prefix, ...substr];
3017
+ return { query, team: matches.length === 1 ? matches[0] : null, matches };
3018
+ }
3019
+
2903
3020
  // src/standings.ts
2904
3021
  function blankRow(team) {
2905
3022
  return {
@@ -2969,6 +3086,7 @@ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2969
3086
  var DEFAULT_COMPETITION = "fifa.world";
2970
3087
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2971
3088
  var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
3089
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2972
3090
  function competitionBase(slug) {
2973
3091
  return `${ESPN_SOCCER}/${slug}`;
2974
3092
  }
@@ -3012,9 +3130,15 @@ function toInt(s) {
3012
3130
  return Number.isFinite(n) ? n : void 0;
3013
3131
  }
3014
3132
  function toTeam(t2) {
3015
- const name = t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD";
3016
- const code = (t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3017
- return { code, name, flag: nationToFlag(t2?.displayName ?? t2?.abbreviation ?? name) };
3133
+ const name = sanitizeFeedText(
3134
+ t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
3135
+ );
3136
+ const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3137
+ return {
3138
+ code,
3139
+ name,
3140
+ flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
3141
+ };
3018
3142
  }
3019
3143
  function mapEspnEvent(ev, ctx = {}) {
3020
3144
  const comp = ev.competitions?.[0];
@@ -3045,9 +3169,9 @@ function mapEspnEvent(ev, ctx = {}) {
3045
3169
  stage,
3046
3170
  group,
3047
3171
  kickoff: ev.date,
3048
- venue: comp?.venue?.fullName ?? "",
3049
- city: comp?.venue?.address?.city || void 0,
3050
- country: comp?.venue?.address?.country || void 0,
3172
+ venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3173
+ city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3174
+ country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
3051
3175
  home,
3052
3176
  away,
3053
3177
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3171,11 +3295,18 @@ var EspnAdapter = class {
3171
3295
  try {
3172
3296
  const res = await doFetch(url, {
3173
3297
  signal: controller.signal,
3298
+ // ESPN never legitimately redirects; following one would sidestep the
3299
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3300
+ redirect: "error",
3174
3301
  headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3175
3302
  });
3176
3303
  if (!res.ok) {
3177
3304
  throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3178
3305
  }
3306
+ const length = Number(res.headers?.get?.("content-length"));
3307
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3308
+ throw new Error(`ESPN response too large: ${length} bytes`);
3309
+ }
3179
3310
  return await res.json();
3180
3311
  } finally {
3181
3312
  clearTimeout(timer);
@@ -4511,12 +4642,19 @@ var PolymarketProvider = class {
4511
4642
  const doFetch = this.opts.fetchImpl ?? fetch;
4512
4643
  const res = await doFetch(url, {
4513
4644
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4645
+ // Gamma never legitimately redirects; following one would sidestep the
4646
+ // host allow-list (it only validates the base URL), so reject redirects.
4647
+ redirect: "error",
4514
4648
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4515
4649
  });
4516
4650
  if (res.status === 404) return void 0;
4517
4651
  if (!res.ok) {
4518
4652
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4519
4653
  }
4654
+ const length = Number(res.headers?.get?.("content-length"));
4655
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4656
+ throw new Error(`Polymarket response too large: ${length} bytes`);
4657
+ }
4520
4658
  const data = await res.json();
4521
4659
  const event = Array.isArray(data) ? data[0] : data;
4522
4660
  return event && typeof event === "object" ? event : void 0;
@@ -4969,6 +5107,7 @@ export {
4969
5107
  DEFAULT_FLAVOR,
4970
5108
  DEFAULT_MAX_AGE_MS,
4971
5109
  EspnAdapter,
5110
+ FEED_TEXT_MAX,
4972
5111
  FLAVOR_LEVELS,
4973
5112
  FakeMarketProvider,
4974
5113
  LIVE_WINDOW_MS,
@@ -4976,6 +5115,7 @@ export {
4976
5115
  SHARE_DISCLAIMER,
4977
5116
  SHARE_HASHTAG,
4978
5117
  allFixtures,
5118
+ allTeams,
4979
5119
  asFlavorLevel,
4980
5120
  buildBracketTopology,
4981
5121
  buildBracketView,
@@ -4986,6 +5126,7 @@ export {
4986
5126
  countdown,
4987
5127
  currentOrNextFixtureForTeam,
4988
5128
  deriveFavorite,
5129
+ displayWidth,
4989
5130
  favoriteStrength,
4990
5131
  fixturesByDate,
4991
5132
  fixturesByGroup,
@@ -5024,6 +5165,7 @@ export {
5024
5165
  liveSourceLabel,
5025
5166
  loadBracketTopology,
5026
5167
  localDate,
5168
+ lookupTeam,
5027
5169
  makeAdapter,
5028
5170
  makeMarketProvider,
5029
5171
  mapEspnEvent,
@@ -5047,12 +5189,15 @@ export {
5047
5189
  normalizeLang,
5048
5190
  normalizeOutcomes,
5049
5191
  outcomeFromScore,
5192
+ padVisible,
5050
5193
  parseStandings,
5051
5194
  parseTeamSlot,
5052
5195
  resolveCompetition,
5053
5196
  resolveMarketSource,
5054
5197
  resolveTz,
5055
5198
  sanitizeBundledFixture,
5199
+ sanitizeFeedText,
5200
+ sanitizeMatchStrings,
5056
5201
  scoreline,
5057
5202
  stageLabel,
5058
5203
  stageLabelI18n,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.8.17",
3
+ "version": "0.8.19",
4
4
  "description": "Domain model, provider adapters (ESPN), standings, bundled schedule, and the read-only Polymarket market-signal sidecar powering Claudinho. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",