@claudinho/cli 0.9.4 → 0.10.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.
Files changed (2) hide show
  1. package/dist/index.js +645 -197
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -274,6 +274,8 @@ var EN = {
274
274
  "bracket.slot.tbd": "TBD",
275
275
  "live.data": "Live data: {source}",
276
276
  "standings.unavailable": "Live standings unavailable.",
277
+ "standings.partial": "Partial table \u2014 {n} rows could not be read.",
278
+ "competition.unsupported": "Not available for this competition yet.",
277
279
  "share.tryIt": "Try it: {line}",
278
280
  "stage.group": "Group {group}",
279
281
  "stage.groupStage": "Group stage",
@@ -304,6 +306,8 @@ var ES = {
304
306
  "bracket.slot.tbd": "Por definir",
305
307
  "live.data": "Datos en vivo: {source}",
306
308
  "standings.unavailable": "Tabla en vivo no disponible.",
309
+ "standings.partial": "Tabla parcial \u2014 no se pudieron leer {n} filas.",
310
+ "competition.unsupported": "A\xFAn no disponible para esta competici\xF3n.",
307
311
  "share.tryIt": "Pru\xE9balo: {line}",
308
312
  "stage.group": "Grupo {group}",
309
313
  "stage.groupStage": "Fase de grupos",
@@ -334,6 +338,8 @@ var PT = {
334
338
  "bracket.slot.tbd": "A definir",
335
339
  "live.data": "Dados ao vivo: {source}",
336
340
  "standings.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
341
+ "standings.partial": "Tabela parcial \u2014 {n} linhas n\xE3o puderam ser lidas.",
342
+ "competition.unsupported": "Ainda n\xE3o dispon\xEDvel para esta competi\xE7\xE3o.",
337
343
  "share.tryIt": "Experimente: {line}",
338
344
  "stage.group": "Grupo {group}",
339
345
  "stage.groupStage": "Fase de grupos",
@@ -364,6 +370,8 @@ var FR = {
364
370
  "bracket.slot.tbd": "\xC0 d\xE9finir",
365
371
  "live.data": "Donn\xE9es en direct : {source}",
366
372
  "standings.unavailable": "Classement en direct indisponible.",
373
+ "standings.partial": "Classement partiel \u2014 {n} lignes n'ont pas pu \xEAtre lues.",
374
+ "competition.unsupported": "Pas encore disponible pour cette comp\xE9tition.",
367
375
  "share.tryIt": "Essayez : {line}",
368
376
  "stage.group": "Groupe {group}",
369
377
  "stage.groupStage": "Phase de groupes",
@@ -2848,11 +2856,13 @@ function fixturesByGroup(group, fixtures = SCHEDULE) {
2848
2856
  const g = group.toUpperCase();
2849
2857
  return fixtures.filter((m) => (m.group ?? "").toUpperCase() === g).sort(byKickoff);
2850
2858
  }
2859
+ function isUpcoming(m, now = /* @__PURE__ */ new Date()) {
2860
+ if (m.status === "CANCELLED" || m.status === "POSTPONED") return false;
2861
+ return Date.parse(m.kickoff) >= now.getTime();
2862
+ }
2851
2863
  function nextFixtureForTeam(code, opts = {}) {
2852
2864
  const from = opts.from ?? /* @__PURE__ */ new Date();
2853
- return fixturesByTeam(code, opts.fixtures ?? SCHEDULE).find(
2854
- (m) => new Date(m.kickoff).getTime() >= from.getTime()
2855
- );
2865
+ return fixturesByTeam(code, opts.fixtures ?? SCHEDULE).find((m) => isUpcoming(m, from));
2856
2866
  }
2857
2867
  var LIVE_WINDOW_MS = 140 * 6e4;
2858
2868
  var KNOCKOUT_EXTRA_TIME_MS = 60 * 6e4;
@@ -2980,8 +2990,8 @@ function humanLabel(value, maxColumns = MAX_LABEL_COLUMNS) {
2980
2990
  const capped = value.length > MAX_LABEL_INPUT_UNITS ? value.slice(0, MAX_LABEL_INPUT_UNITS) : value;
2981
2991
  return visible(runToFixedPoint(capped, maxColumns));
2982
2992
  }
2983
- function visible(label) {
2984
- return label !== "" && displayWidth(label) === 0 ? "" : label;
2993
+ function visible(label2) {
2994
+ return label2 !== "" && displayWidth(label2) === 0 ? "" : label2;
2985
2995
  }
2986
2996
  function runToFixedPoint(capped, maxColumns) {
2987
2997
  const first = sealLabelOnce(capped, maxColumns);
@@ -3209,7 +3219,7 @@ function toParticipant(raw) {
3209
3219
  const providerId = opaqueId(raw.team?.id, ESPN_ID);
3210
3220
  const known = productFlag(name) !== nationToFlag("");
3211
3221
  return valid(
3212
- providerId && known ? { kind: "team", providerId, team } : { kind: "slot", team }
3222
+ providerId && known ? { kind: "team", providerId, team } : { kind: "slot", ...providerId ? { providerId } : {}, team }
3213
3223
  );
3214
3224
  }
3215
3225
  function mapStatus(st) {
@@ -3278,7 +3288,7 @@ function parseEspnEvent(raw, ctx = {}) {
3278
3288
  if (awayP.kind !== "valid") return awayP;
3279
3289
  const h = homeP.value;
3280
3290
  const a = awayP.value;
3281
- if (h.kind === "team" && a.kind === "team" && h.providerId === a.providerId) {
3291
+ if (h.providerId !== void 0 && h.providerId === a.providerId) {
3282
3292
  return definitiveNone("both competitors are the same team");
3283
3293
  }
3284
3294
  const home = homeP.value.team;
@@ -3430,9 +3440,13 @@ function parseEspnStandings(raw) {
3430
3440
  const seenGroups = /* @__PURE__ */ new Set();
3431
3441
  const seenProviderIds = /* @__PURE__ */ new Set();
3432
3442
  for (const child of children) {
3433
- const label = humanLabel(child?.name ?? child?.abbreviation);
3434
- const letter = label.match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3435
- if (!letter) continue;
3443
+ const label2 = humanLabel(child?.name ?? child?.abbreviation);
3444
+ const letter = label2.match(/Group\s+([A-L])(?![A-Za-z0-9])/i)?.[1]?.toUpperCase();
3445
+ if (!letter) {
3446
+ const rows = child?.standings?.entries;
3447
+ if (Array.isArray(rows) && rows.length > 0) complete = false;
3448
+ continue;
3449
+ }
3436
3450
  if (seenGroups.has(letter)) {
3437
3451
  complete = false;
3438
3452
  continue;
@@ -3443,30 +3457,37 @@ function parseEspnStandings(raw) {
3443
3457
  complete = false;
3444
3458
  continue;
3445
3459
  }
3460
+ let omitted = 0;
3446
3461
  if (rawEntries.length > MAX_GROUP_ROWS) {
3447
3462
  rowsTruncated = true;
3448
3463
  complete = false;
3464
+ omitted += rawEntries.length - MAX_GROUP_ROWS;
3449
3465
  }
3450
3466
  const entries = takeBounded(rawEntries, MAX_GROUP_ROWS);
3451
- const seenTeams = /* @__PURE__ */ new Set();
3467
+ const seenCodes = /* @__PURE__ */ new Map();
3452
3468
  const seenRanks = /* @__PURE__ */ new Set();
3453
3469
  const ranked = [];
3454
3470
  for (const e of entries) {
3455
3471
  const r = entryToRow(e);
3456
3472
  if (r.kind !== "valid") {
3457
3473
  if (r.kind !== "definitive-none") complete = false;
3474
+ omitted += 1;
3458
3475
  continue;
3459
3476
  }
3460
- const key = r.value.providerId ?? r.value.team.code;
3461
- if (seenTeams.has(key) || seenRanks.has(r.value.providerRank) || r.value.providerId !== void 0 && seenProviderIds.has(r.value.providerId)) {
3477
+ const { providerId, providerRank } = r.value;
3478
+ const code = r.value.team.code;
3479
+ const priorHadId = seenCodes.get(code);
3480
+ const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
3481
+ if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
3462
3482
  complete = false;
3483
+ omitted += 1;
3463
3484
  continue;
3464
3485
  }
3465
- seenTeams.add(key);
3466
- seenRanks.add(r.value.providerRank);
3467
- if (r.value.providerId !== void 0) seenProviderIds.add(r.value.providerId);
3486
+ seenCodes.set(code, providerId !== void 0);
3487
+ seenRanks.add(providerRank);
3488
+ if (providerId !== void 0) seenProviderIds.add(providerId);
3468
3489
  const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3469
- ranked.push({ row, rank });
3490
+ ranked.push({ row: { ...row, rank }, rank });
3470
3491
  }
3471
3492
  ranked.sort((a, b) => {
3472
3493
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
@@ -3478,7 +3499,11 @@ function parseEspnStandings(raw) {
3478
3499
  complete = false;
3479
3500
  continue;
3480
3501
  }
3481
- out2.push({ group: letter, rows: ranked.map((x) => x.row) });
3502
+ out2.push({
3503
+ group: letter,
3504
+ rows: ranked.map((x) => x.row),
3505
+ ...omitted > 0 ? { partial: { omitted } } : {}
3506
+ });
3482
3507
  }
3483
3508
  return {
3484
3509
  items: out2,
@@ -3489,11 +3514,72 @@ function parseEspnStandings(raw) {
3489
3514
  complete: complete && !rowsTruncated
3490
3515
  };
3491
3516
  }
3517
+ var ResponseTooLargeError = class extends Error {
3518
+ constructor(bytes, limit) {
3519
+ super(`response body exceeds ${limit} bytes (${bytes} seen)`);
3520
+ this.bytes = bytes;
3521
+ this.limit = limit;
3522
+ this.name = "ResponseTooLargeError";
3523
+ }
3524
+ bytes;
3525
+ limit;
3526
+ };
3527
+ function isStream(v) {
3528
+ return typeof v?.getReader === "function";
3529
+ }
3530
+ async function readJsonBounded(res, maxBytes) {
3531
+ const r = res;
3532
+ const declared = Number(r.headers?.get?.("content-length"));
3533
+ if (Number.isFinite(declared) && declared > maxBytes) {
3534
+ if (isStream(r.body)) await r.body.cancel().catch(() => {
3535
+ });
3536
+ throw new ResponseTooLargeError(declared, maxBytes);
3537
+ }
3538
+ if (isStream(r.body)) {
3539
+ const reader = r.body.getReader();
3540
+ const chunks = [];
3541
+ let total = 0;
3542
+ for (; ; ) {
3543
+ const { done, value } = await reader.read();
3544
+ if (done) break;
3545
+ total += value.byteLength;
3546
+ if (total > maxBytes) {
3547
+ await reader.cancel().catch(() => {
3548
+ });
3549
+ throw new ResponseTooLargeError(total, maxBytes);
3550
+ }
3551
+ chunks.push(value);
3552
+ }
3553
+ return JSON.parse(new TextDecoder().decode(Buffer.concat(chunks)));
3554
+ }
3555
+ if (typeof r.text === "function") {
3556
+ const text = await r.text();
3557
+ const size = Buffer.byteLength(text);
3558
+ if (size > maxBytes) throw new ResponseTooLargeError(size, maxBytes);
3559
+ return JSON.parse(text);
3560
+ }
3561
+ if (typeof r.json === "function") return r.json();
3562
+ throw new TypeError("response has no readable body");
3563
+ }
3492
3564
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3493
3565
  var DEFAULT_COMPETITION = "fifa.world";
3494
3566
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3495
- var USER_AGENT = `claudinho/${"0.9.4"} (+https://github.com/arturogarrido/claudinho)`;
3567
+ var USER_AGENT = `claudinho/${"0.10.1"} (+https://github.com/arturogarrido/claudinho)`;
3496
3568
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3569
+ var DEFAULT_COOLDOWN_MS = 5 * 6e4;
3570
+ var MAX_COOLDOWN_MS = 15 * 6e4;
3571
+ function retryAfterMs(header2, nowMs) {
3572
+ if (typeof header2 !== "string" || header2.trim() === "") return DEFAULT_COOLDOWN_MS;
3573
+ const h = header2.trim();
3574
+ let ms;
3575
+ if (/^\d+$/.test(h)) ms = Number(h) * 1e3;
3576
+ else {
3577
+ const at = Date.parse(h);
3578
+ if (Number.isFinite(at)) ms = at - nowMs;
3579
+ }
3580
+ if (ms === void 0 || !Number.isFinite(ms)) return DEFAULT_COOLDOWN_MS;
3581
+ return Math.min(Math.max(ms, 0), MAX_COOLDOWN_MS);
3582
+ }
3497
3583
  function competitionBase(slug) {
3498
3584
  return `${ESPN_SOCCER}/${slug}`;
3499
3585
  }
@@ -3502,6 +3588,8 @@ var STANDINGS_SHARE_MS = 3e4;
3502
3588
  var ProviderError = class extends Error {
3503
3589
  kind;
3504
3590
  status;
3591
+ /** For a throttle: how long the adapter will refuse to fetch (bounded). */
3592
+ retryAfterMs;
3505
3593
  constructor(message, kind, status) {
3506
3594
  super(message);
3507
3595
  this.name = "ProviderError";
@@ -3525,6 +3613,7 @@ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length
3525
3613
  var EspnAdapter = class {
3526
3614
  constructor(opts = {}) {
3527
3615
  this.opts = opts;
3616
+ this.clock = opts.now ?? (() => Date.now());
3528
3617
  const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3529
3618
  this.expectedStandingsGroups = expected ? [...expected] : void 0;
3530
3619
  this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
@@ -3550,6 +3639,53 @@ var EspnAdapter = class {
3550
3639
  * throttle (persist a backoff) from an ordinary blip.
3551
3640
  */
3552
3641
  lastError;
3642
+ /**
3643
+ * A retained throttle (audit A12): after a 429/403 every call inside the
3644
+ * window throws the provider's last answer WITHOUT a request. A server-
3645
+ * lifetime MCP adapter is covered by this alone; the CLI pre-arms each
3646
+ * process from its persisted cache via `armCooldown`.
3647
+ */
3648
+ cooldownUntilMs;
3649
+ cooldownError;
3650
+ cooldownListeners = /* @__PURE__ */ new Set();
3651
+ clock;
3652
+ /** Epoch ms until which requests are refused, when a cooldown is armed. */
3653
+ get cooldownUntil() {
3654
+ return this.cooldownUntilMs;
3655
+ }
3656
+ /**
3657
+ * Arm the cooldown from outside — a fresh CLI process reading the backoff
3658
+ * its refresher persisted. The retained error reads as a throttle so every
3659
+ * caller's `degraded` path and the refresher's persistence treat it as one.
3660
+ */
3661
+ armCooldown(untilMs, reason) {
3662
+ const nowMs = this.clock();
3663
+ const error = reason ?? new ProviderError("ESPN request skipped: provider cooldown in effect", "http", 429);
3664
+ error.retryAfterMs = Math.max(0, untilMs - nowMs);
3665
+ this.arm(untilMs, error);
3666
+ }
3667
+ /**
3668
+ * Be told whenever the cooldown window is armed or EXTENDED — the way a
3669
+ * caller persists a throttle that arrives from a still-running request after
3670
+ * its own call already returned (review P2 on #128). Returns unsubscribe.
3671
+ */
3672
+ onCooldown(listener) {
3673
+ this.cooldownListeners.add(listener);
3674
+ return () => {
3675
+ this.cooldownListeners.delete(listener);
3676
+ };
3677
+ }
3678
+ /**
3679
+ * The ONE place a window is set. Concurrent requests can each carry a
3680
+ * Retry-After; the LATEST expiry wins — a shorter one arriving second must
3681
+ * never shorten a longer active window (review P2 on #128).
3682
+ */
3683
+ arm(untilMs, error) {
3684
+ if (this.cooldownUntilMs !== void 0 && untilMs <= this.cooldownUntilMs) return;
3685
+ this.cooldownUntilMs = untilMs;
3686
+ this.cooldownError = error;
3687
+ for (const listener of this.cooldownListeners) listener(untilMs);
3688
+ }
3553
3689
  async fetchByDate(dateISO) {
3554
3690
  return this.fetchScoreboard(toEspnDate(dateISO));
3555
3691
  }
@@ -3637,6 +3773,11 @@ var EspnAdapter = class {
3637
3773
  return usableProviderItems("scoreboard", parsed);
3638
3774
  }
3639
3775
  async get(url) {
3776
+ const nowMs = this.clock();
3777
+ if (this.cooldownError && this.cooldownUntilMs !== void 0 && nowMs < this.cooldownUntilMs) {
3778
+ this.lastError = this.cooldownError;
3779
+ throw this.cooldownError;
3780
+ }
3640
3781
  const doFetch = this.opts.fetchImpl ?? fetch;
3641
3782
  const controller = new AbortController();
3642
3783
  const timer = setTimeout(
@@ -3658,19 +3799,24 @@ var EspnAdapter = class {
3658
3799
  throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3659
3800
  }
3660
3801
  if (!res.ok) {
3661
- throw new ProviderError(
3802
+ const pe = new ProviderError(
3662
3803
  `ESPN request failed: ${res.status} ${res.statusText}`,
3663
3804
  "http",
3664
3805
  res.status
3665
3806
  );
3666
- }
3667
- const length = Number(res.headers?.get?.("content-length"));
3668
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3669
- throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
3807
+ if (pe.throttled) {
3808
+ const receiptMs = this.clock();
3809
+ pe.retryAfterMs = retryAfterMs(res.headers?.get?.("retry-after"), receiptMs);
3810
+ this.arm(receiptMs + pe.retryAfterMs, pe);
3811
+ }
3812
+ throw pe;
3670
3813
  }
3671
3814
  try {
3672
- return await res.json();
3815
+ return await readJsonBounded(res, MAX_RESPONSE_BYTES);
3673
3816
  } catch (e) {
3817
+ if (e instanceof ResponseTooLargeError) {
3818
+ throw new ProviderError(`ESPN response too large: ${e.bytes} bytes`, "parse");
3819
+ }
3674
3820
  throw new ProviderError(
3675
3821
  `ESPN response unparseable: ${e?.message ?? e}`,
3676
3822
  "parse"
@@ -3713,14 +3859,30 @@ function hasGroupStarted(group, tables) {
3713
3859
  function matchesPerTeamInGroup(teamCount) {
3714
3860
  return Math.max(0, teamCount - 1);
3715
3861
  }
3716
- function isGroupStandingsComplete(table) {
3717
- const n = table?.rows.length ?? 0;
3862
+ function isGroupStandingsComplete(table, expectedTeams) {
3863
+ if (!table || table.partial) return false;
3864
+ const n = table.rows.length;
3718
3865
  if (n < 2) return false;
3719
- const required = matchesPerTeamInGroup(n);
3866
+ if (expectedTeams !== void 0 && n < expectedTeams) return false;
3867
+ const required = matchesPerTeamInGroup(Math.max(n, expectedTeams ?? n));
3720
3868
  return table.rows.every((r) => r.played >= required);
3721
3869
  }
3870
+ function bundledGroupSize(group) {
3871
+ const codes = /* @__PURE__ */ new Set();
3872
+ for (const m of fixturesByGroup(group)) {
3873
+ codes.add(m.home.code);
3874
+ codes.add(m.away.code);
3875
+ }
3876
+ return codes.size > 0 ? codes.size : void 0;
3877
+ }
3722
3878
  function isGroupComplete(group, tables) {
3723
- return isGroupStandingsComplete(tables.find((t2) => t2.group === group));
3879
+ return isGroupStandingsComplete(
3880
+ tables.find((t2) => t2.group === group),
3881
+ bundledGroupSize(group)
3882
+ );
3883
+ }
3884
+ function isGroupPartial(group, tables) {
3885
+ return tables.find((t2) => t2.group === group)?.partial !== void 0;
3724
3886
  }
3725
3887
  function resolveWinner(match) {
3726
3888
  if (!isFinished(match.status)) return void 0;
@@ -3754,8 +3916,8 @@ function resolveLoser(match) {
3754
3916
  function participant(team, status) {
3755
3917
  return { label: team.name, flag: team.flag, code: team.code, status };
3756
3918
  }
3757
- function tbd(label) {
3758
- return { label, flag: "\u{1F3F3}\uFE0F", status: "tbd" };
3919
+ function tbd(label2) {
3920
+ return { label: label2, flag: "\u{1F3F3}\uFE0F", status: "tbd" };
3759
3921
  }
3760
3922
  function confirmedLiveParticipant(team) {
3761
3923
  if (!team || team.flag === "\u{1F3F3}\uFE0F") return void 0;
@@ -3775,15 +3937,15 @@ function resolveSlot(ref, ctx, liveTeam, fixtureInMergedSet = false) {
3775
3937
  return tbd(ref.label);
3776
3938
  case "group": {
3777
3939
  if (liveParticipant) return liveParticipant;
3778
- if (!ctx.standingsDegraded && hasGroupStarted(ref.group, ctx.tables)) {
3940
+ if (!ctx.standingsDegraded && !isGroupPartial(ref.group, ctx.tables) && hasGroupStarted(ref.group, ctx.tables)) {
3779
3941
  const team = teamFromStandings(ref.group, ref.position, ctx.tables);
3780
3942
  if (team) {
3781
3943
  const status = isGroupComplete(ref.group, ctx.tables) ? "confirmed" : "projected";
3782
3944
  return participant(team, status);
3783
3945
  }
3784
3946
  }
3785
- const label = ref.position === 1 ? t(ctx.lang, "bracket.slot.groupWinner", { group: ref.group }) : t(ctx.lang, "bracket.slot.groupSecond", { group: ref.group });
3786
- return tbd(label);
3947
+ const label2 = ref.position === 1 ? t(ctx.lang, "bracket.slot.groupWinner", { group: ref.group }) : t(ctx.lang, "bracket.slot.groupSecond", { group: ref.group });
3948
+ return tbd(label2);
3787
3949
  }
3788
3950
  case "third":
3789
3951
  if (liveParticipant) return liveParticipant;
@@ -4409,13 +4571,17 @@ function resolveCompetition(explicit) {
4409
4571
  }
4410
4572
  return DEFAULT_COMPETITION;
4411
4573
  }
4574
+ var BUNDLE_COMPETITION = DEFAULT_COMPETITION;
4575
+ function bundleApplies(competition = resolveCompetition()) {
4576
+ return competition === BUNDLE_COMPETITION;
4577
+ }
4412
4578
  var KNOWN_SOURCES = ["espn"];
4413
- function makeAdapter(source = "espn") {
4579
+ function makeAdapter(source = "espn", opts = {}) {
4414
4580
  switch (source) {
4415
4581
  case "espn": {
4416
4582
  const competition = resolveCompetition();
4417
4583
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
4418
- return new EspnAdapter({ baseUrl });
4584
+ return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups, now: opts.now });
4419
4585
  }
4420
4586
  default:
4421
4587
  throw new Error(
@@ -4433,7 +4599,7 @@ function liveSourceLabel(source) {
4433
4599
  return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
4434
4600
  }
4435
4601
  async function getMatchesForDate(adapter, dateISO) {
4436
- const base = allFixtures();
4602
+ const base = bundleApplies() ? allFixtures() : [];
4437
4603
  const day = dateISO.slice(0, 10);
4438
4604
  try {
4439
4605
  const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
@@ -4478,21 +4644,27 @@ function knockoutWindow() {
4478
4644
  return knockoutWindowMemo;
4479
4645
  }
4480
4646
  async function getBracket(adapter, opts = {}) {
4647
+ if (!bundleApplies()) {
4648
+ const view2 = { stages: [], degraded: false, standingsDegraded: false, unsupported: true };
4649
+ return { view: view2, degraded: false, standingsDegraded: false, unsupported: true };
4650
+ }
4481
4651
  const topology = loadBracketTopology();
4482
4652
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
4483
4653
  let matches = base;
4484
4654
  let liveDegraded = true;
4485
4655
  let source;
4486
- try {
4487
- const win = knockoutWindow();
4488
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4489
- matches = mergeLive(base, live);
4490
- liveDegraded = false;
4491
- source = adapter.name;
4492
- } catch {
4656
+ const win = knockoutWindow();
4657
+ if (adapter.fetchWindow && win) {
4658
+ try {
4659
+ const live = await adapter.fetchWindow(win.start, win.end);
4660
+ matches = mergeLive(base, live);
4661
+ liveDegraded = false;
4662
+ source = adapter.name;
4663
+ } catch {
4664
+ }
4493
4665
  }
4494
4666
  const standings = await getStandings(adapter);
4495
- if (!source && !standings.degraded && standings.source) {
4667
+ if (!source && !standings.degraded && standings.source && standings.tables.length > 0) {
4496
4668
  source = standings.source;
4497
4669
  }
4498
4670
  const view = buildBracketView(
@@ -4514,18 +4686,18 @@ async function getBracket(adapter, opts = {}) {
4514
4686
  }
4515
4687
  var EXTRA_TIME_SLACK_MS = 60 * 6e4;
4516
4688
  async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
4689
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4517
4690
  const nowMs = now.getTime();
4518
4691
  let fixtures = allFixtures();
4519
4692
  let overlayFailed = false;
4520
- try {
4521
- const win = knockoutWindow();
4522
- if (adapter.fetchWindow && win) {
4523
- fixtures = mergeLive(
4524
- fixtures,
4525
- await adapter.fetchWindow(win.start, win.end)
4526
- );
4693
+ const win = knockoutWindow();
4694
+ if (adapter.fetchWindow && win) {
4695
+ try {
4696
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4697
+ } catch {
4698
+ overlayFailed = true;
4527
4699
  }
4528
- } catch {
4700
+ } else {
4529
4701
  overlayFailed = true;
4530
4702
  }
4531
4703
  const candidate = fixturesByTeam(code, fixtures).find((m) => {
@@ -4541,23 +4713,27 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4541
4713
  return { match: next, degraded: overlayFailed };
4542
4714
  }
4543
4715
  async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
4716
+ if (!bundleApplies()) return { fixture: void 0, degraded: false, unsupported: true };
4544
4717
  const base = allFixtures();
4545
4718
  let matches = base;
4546
4719
  let degraded = true;
4547
4720
  let liveById;
4548
- try {
4549
- const win = knockoutWindow();
4550
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4551
- matches = mergeLive(base, live);
4552
- degraded = false;
4553
- liveById = new Set(live.map((m) => m.id));
4554
- } catch {
4721
+ const win = knockoutWindow();
4722
+ if (adapter.fetchWindow && win) {
4723
+ try {
4724
+ const live = await adapter.fetchWindow(win.start, win.end);
4725
+ matches = mergeLive(base, live);
4726
+ degraded = false;
4727
+ liveById = new Set(live.map((m) => m.id));
4728
+ } catch {
4729
+ }
4555
4730
  }
4556
4731
  const fixture = nextFixtureForTeam(code, { from: now, fixtures: matches });
4557
4732
  const source = fixture && liveById?.has(fixture.id) ? adapter.name : void 0;
4558
4733
  return { fixture, degraded, source };
4559
4734
  }
4560
4735
  async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
4736
+ if (!bundleApplies()) return { fixtures: [], degraded: true, unsupported: true };
4561
4737
  const win = knockoutWindow();
4562
4738
  if (!adapter.fetchWindow || !win) return { fixtures: [], degraded: true };
4563
4739
  let live;
@@ -4566,13 +4742,13 @@ async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
4566
4742
  } catch {
4567
4743
  return { fixtures: [], degraded: true };
4568
4744
  }
4569
- const nowMs = now.getTime();
4570
4745
  const fixtures = live.filter(
4571
- (m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY" && Date.parse(m.kickoff) >= nowMs && isResolvedNation(m.home) && isResolvedNation(m.away)
4746
+ (m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY" && isUpcoming(m, now) && isResolvedNation(m.home) && isResolvedNation(m.away)
4572
4747
  ).sort(byKickoff);
4573
4748
  return { fixtures, degraded: false };
4574
4749
  }
4575
4750
  async function getMatchById(adapter, id) {
4751
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4576
4752
  const base = allFixtures().find((m) => m.id === id);
4577
4753
  if (!base) return { match: void 0, degraded: false };
4578
4754
  const day = base.kickoff.slice(0, 10);
@@ -5013,11 +5189,15 @@ var PolymarketProvider = class {
5013
5189
  if (!res.ok) {
5014
5190
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
5015
5191
  }
5016
- const length = Number(res.headers?.get?.("content-length"));
5017
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
5018
- throw new Error(`Polymarket response too large: ${length} bytes`);
5192
+ let data;
5193
+ try {
5194
+ data = await readJsonBounded(res, MAX_RESPONSE_BYTES);
5195
+ } catch (e) {
5196
+ if (e instanceof ResponseTooLargeError) {
5197
+ throw new Error(`Polymarket response too large: ${e.bytes} bytes`);
5198
+ }
5199
+ throw e;
5019
5200
  }
5020
- const data = await res.json();
5021
5201
  if (Array.isArray(data) && data.length > 1) {
5022
5202
  return ambiguous("slug returned more than one event");
5023
5203
  }
@@ -5097,7 +5277,7 @@ var PolymarketProvider = class {
5097
5277
  const outcomes = [];
5098
5278
  let asOf = canonicalTimestamp(event.updatedAt);
5099
5279
  let liquidity;
5100
- for (const [kind, market, teamCode2, label] of legs) {
5280
+ for (const [kind, market, teamCode2, label2] of legs) {
5101
5281
  if (!market) continue;
5102
5282
  if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
5103
5283
  return malformed("market active/closed is not a boolean");
@@ -5110,7 +5290,7 @@ var PolymarketProvider = class {
5110
5290
  }
5111
5291
  const yes = yesPrice(market);
5112
5292
  if (yes == null) return malformed("market is not a readable Yes/No binary");
5113
- outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
5293
+ outcomes.push({ kind, teamCode: teamCode2, label: label2, probability: yes });
5114
5294
  const marketAsOf = canonicalTimestamp(market.updatedAt);
5115
5295
  if (!marketAsOf) {
5116
5296
  return malformed("market updatedAt missing or unparseable");
@@ -5278,6 +5458,10 @@ function numberish(v) {
5278
5458
  }
5279
5459
  return void 0;
5280
5460
  }
5461
+ var MARKET_COMPETITIONS = /* @__PURE__ */ new Set([DEFAULT_COMPETITION]);
5462
+ function marketsCoverCompetition(competition = resolveCompetition()) {
5463
+ return MARKET_COMPETITIONS.has(competition);
5464
+ }
5281
5465
  function resolveMarketSource(explicit) {
5282
5466
  if (explicit) return explicit;
5283
5467
  if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
@@ -5294,6 +5478,7 @@ function makeMarketProvider(source) {
5294
5478
  return new FakeMarketProvider();
5295
5479
  // no synth → yields no signals, no network
5296
5480
  default:
5481
+ if (!marketsCoverCompetition()) return new FakeMarketProvider();
5297
5482
  return new PolymarketProvider();
5298
5483
  }
5299
5484
  }
@@ -5419,10 +5604,13 @@ function formatShareTable(input, options = {}) {
5419
5604
  input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5420
5605
  );
5421
5606
  } else {
5422
- for (const { group, rows } of input.tables) {
5423
- blocks.push(
5424
- [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
5425
- );
5607
+ for (const { group, rows, partial } of input.tables) {
5608
+ const lines = [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, r.rank ?? i + 1))];
5609
+ if (partial) {
5610
+ const n = partial.omitted;
5611
+ lines.push("", `(partial table \u2014 ${n} row${n === 1 ? "" : "s"} unreadable; positions are the provider's ranks)`);
5612
+ }
5613
+ blocks.push(lines.join("\n"));
5426
5614
  }
5427
5615
  if (input.degraded) {
5428
5616
  blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
@@ -5606,11 +5794,56 @@ function resolveConfig(opts) {
5606
5794
 
5607
5795
  // src/cursorPayload.ts
5608
5796
  import { readFileSync } from "fs";
5797
+ var MAX_CURSOR_PAYLOAD_BYTES = 64 * 1024;
5798
+ var MAX_CURSOR_LABEL_COLUMNS = 40;
5799
+ function label(v) {
5800
+ const s = humanLabel(v, MAX_CURSOR_LABEL_COLUMNS);
5801
+ return s === "" ? void 0 : s;
5802
+ }
5803
+ function finite(v) {
5804
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
5805
+ }
5806
+ function record(v) {
5807
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
5808
+ }
5809
+ function validateCursorPayload(parsed) {
5810
+ const p = record(parsed);
5811
+ if (!p) return void 0;
5812
+ const out2 = {};
5813
+ const model = record(p.model);
5814
+ if (model) {
5815
+ const display_name = label(model.display_name);
5816
+ const param_summary = label(model.param_summary);
5817
+ if (display_name || param_summary) {
5818
+ out2.model = {
5819
+ ...display_name ? { display_name } : {},
5820
+ ...param_summary ? { param_summary } : {}
5821
+ };
5822
+ }
5823
+ }
5824
+ const cw = record(p.context_window);
5825
+ if (cw) {
5826
+ if (cw.used_percentage === null) out2.context_window = { used_percentage: null };
5827
+ else {
5828
+ const pct2 = finite(cw.used_percentage);
5829
+ if (pct2 !== void 0) out2.context_window = { used_percentage: pct2 };
5830
+ }
5831
+ }
5832
+ const wt = record(p.worktree);
5833
+ const name = wt ? label(wt.name) : void 0;
5834
+ if (name) out2.worktree = { name };
5835
+ const vim = record(p.vim);
5836
+ const mode = vim ? label(vim.mode) : void 0;
5837
+ if (mode) out2.vim = { mode };
5838
+ const width = finite(p.render_width_chars);
5839
+ if (width !== void 0 && width > 0) out2.render_width_chars = Math.floor(width);
5840
+ return out2;
5841
+ }
5609
5842
  function parseCursorPayload(raw) {
5610
5843
  try {
5611
5844
  const trimmed = raw.trim();
5612
5845
  if (!trimmed) return void 0;
5613
- return JSON.parse(trimmed);
5846
+ return validateCursorPayload(JSON.parse(trimmed));
5614
5847
  } catch {
5615
5848
  return void 0;
5616
5849
  }
@@ -5623,19 +5856,27 @@ function readCursorPayload() {
5623
5856
  return void 0;
5624
5857
  }
5625
5858
  }
5626
- function readCursorPayloadBounded(timeoutMs = 100, stdin = process.stdin) {
5859
+ function readCursorPayloadBounded(timeoutMs = 100, stdin = process.stdin, maxBytes = MAX_CURSOR_PAYLOAD_BYTES) {
5627
5860
  if (stdin.isTTY) return Promise.resolve(void 0);
5628
5861
  return new Promise((resolve) => {
5629
5862
  const chunks = [];
5863
+ let total = 0;
5864
+ let overflow = false;
5630
5865
  const done = () => {
5631
5866
  clearTimeout(timer);
5632
5867
  stdin.off("data", onData);
5633
5868
  stdin.off("end", done);
5634
5869
  stdin.off("error", done);
5635
5870
  stdin.pause();
5636
- resolve(parseCursorPayload(Buffer.concat(chunks).toString("utf8")));
5871
+ resolve(overflow ? void 0 : parseCursorPayload(Buffer.concat(chunks).toString("utf8")));
5637
5872
  };
5638
5873
  const onData = (c) => {
5874
+ total += c.length;
5875
+ if (total > maxBytes) {
5876
+ overflow = true;
5877
+ done();
5878
+ return;
5879
+ }
5639
5880
  chunks.push(c);
5640
5881
  };
5641
5882
  const timer = setTimeout(done, timeoutMs);
@@ -5658,18 +5899,19 @@ function cursorMetaEnabled(payload) {
5658
5899
  }
5659
5900
  function renderCursorMetaLine(payload) {
5660
5901
  const parts = [];
5661
- const model = payload.model?.display_name;
5902
+ const model = label(payload.model?.display_name);
5662
5903
  if (model) {
5663
- let label = model;
5664
- if (payload.model?.param_summary) label += ` ${payload.model.param_summary}`;
5665
- parts.push(label);
5904
+ const summary = label(payload.model?.param_summary);
5905
+ parts.push(summary ? `${model} ${summary}` : model);
5666
5906
  }
5667
5907
  const pct2 = payload.context_window?.used_percentage;
5668
5908
  if (typeof pct2 === "number" && Number.isFinite(pct2)) {
5669
5909
  parts.push(`ctx ${Math.floor(pct2)}%`);
5670
5910
  }
5671
- if (payload.worktree?.name) parts.push(`wt ${payload.worktree.name}`);
5672
- if (payload.vim?.mode) parts.push(payload.vim.mode);
5911
+ const wt = label(payload.worktree?.name);
5912
+ if (wt) parts.push(`wt ${wt}`);
5913
+ const vim = label(payload.vim?.mode);
5914
+ if (vim) parts.push(vim);
5673
5915
  if (parts.length === 0) return void 0;
5674
5916
  return `\x1B[90m${parts.join(" ")}\x1B[0m`;
5675
5917
  }
@@ -5703,6 +5945,8 @@ var EN2 = {
5703
5945
  "table.degraded": "Live standings unavailable \u2014 showing the group roster.",
5704
5946
  "table.unavailable": "Live standings unavailable.",
5705
5947
  "table.empty": "No standings available.",
5948
+ "table.partial": "Partial table \u2014 {n} rows could not be read.",
5949
+ "competition.unsupported": "Not available for this competition yet.",
5706
5950
  "match.none": "No match found with id {id}.",
5707
5951
  "status.live": "LIVE",
5708
5952
  "status.ht": "HT",
@@ -5744,6 +5988,8 @@ var ES2 = {
5744
5988
  "table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
5745
5989
  "table.unavailable": "Tabla en vivo no disponible.",
5746
5990
  "table.empty": "No hay clasificaci\xF3n disponible.",
5991
+ "table.partial": "Tabla parcial \u2014 no se pudieron leer {n} filas.",
5992
+ "competition.unsupported": "A\xFAn no disponible para esta competici\xF3n.",
5747
5993
  "match.none": "No se encontr\xF3 partido con id {id}.",
5748
5994
  "status.live": "EN VIVO",
5749
5995
  "status.ht": "DESC",
@@ -5785,6 +6031,8 @@ var PT2 = {
5785
6031
  "table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
5786
6032
  "table.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
5787
6033
  "table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
6034
+ "table.partial": "Tabela parcial \u2014 {n} linhas n\xE3o puderam ser lidas.",
6035
+ "competition.unsupported": "Ainda n\xE3o dispon\xEDvel para esta competi\xE7\xE3o.",
5788
6036
  "match.none": "Nenhum jogo encontrado com id {id}.",
5789
6037
  "status.live": "AO VIVO",
5790
6038
  "status.ht": "INT",
@@ -5826,6 +6074,8 @@ var FR2 = {
5826
6074
  "table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
5827
6075
  "table.unavailable": "Classement en direct indisponible.",
5828
6076
  "table.empty": "Aucun classement disponible.",
6077
+ "table.partial": "Classement partiel \u2014 {n} lignes n'ont pas pu \xEAtre lues.",
6078
+ "competition.unsupported": "Pas encore disponible pour cette comp\xE9tition.",
5829
6079
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
5830
6080
  "status.live": "DIRECT",
5831
6081
  "status.ht": "MT",
@@ -5935,7 +6185,7 @@ function dataSource(source, lang, c) {
5935
6185
  }
5936
6186
 
5937
6187
  // src/marketCache.ts
5938
- import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
6188
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
5939
6189
  import { join as join3 } from "path";
5940
6190
 
5941
6191
  // src/cache.ts
@@ -5944,30 +6194,68 @@ import {
5944
6194
  mkdirSync as mkdirSync2,
5945
6195
  openSync as openSync2,
5946
6196
  readFileSync as readFileSync2,
5947
- rmSync,
5948
- statSync,
6197
+ rmSync as rmSync2,
6198
+ statSync as statSync2,
5949
6199
  writeSync as writeSync2
5950
6200
  } from "fs";
5951
6201
  import { join as join2 } from "path";
6202
+ import { randomBytes as randomBytes2 } from "crypto";
5952
6203
 
5953
6204
  // src/paths.ts
5954
- import { closeSync, mkdirSync, openSync, renameSync, writeSync } from "fs";
6205
+ import { randomBytes } from "crypto";
6206
+ import {
6207
+ closeSync,
6208
+ fchmodSync,
6209
+ lstatSync,
6210
+ mkdirSync,
6211
+ openSync,
6212
+ realpathSync,
6213
+ renameSync,
6214
+ rmSync,
6215
+ statSync,
6216
+ writeSync
6217
+ } from "fs";
5955
6218
  import { homedir } from "os";
5956
6219
  import { dirname, join } from "path";
5957
6220
  function cacheDir() {
5958
6221
  const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
5959
6222
  return join(base, "claudinho");
5960
6223
  }
5961
- function writeFileAtomic(path, data) {
6224
+ function writeFileAtomic(path, data, opts = {}) {
5962
6225
  mkdirSync(dirname(path), { recursive: true });
5963
- const tmp = `${path}.${process.pid}.tmp`;
5964
- const fd = openSync(tmp, "w");
6226
+ let target = path;
6227
+ let existingMode;
6228
+ let entry;
6229
+ try {
6230
+ entry = lstatSync(path);
6231
+ } catch {
6232
+ entry = void 0;
6233
+ }
6234
+ if (entry?.isSymbolicLink()) {
6235
+ if (opts.followSymlinks) {
6236
+ target = realpathSync(path);
6237
+ existingMode = statSync(target).mode & 511;
6238
+ }
6239
+ } else if (entry) {
6240
+ existingMode = entry.mode & 511;
6241
+ }
6242
+ const tmp = `${target}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
6243
+ const fd = openSync(tmp, "wx", existingMode ?? opts.mode ?? 438);
5965
6244
  try {
6245
+ if (existingMode !== void 0) fchmodSync(fd, existingMode);
5966
6246
  writeSync(fd, data);
5967
- } finally {
6247
+ } catch (e) {
5968
6248
  closeSync(fd);
6249
+ rmSync(tmp, { force: true });
6250
+ throw e;
6251
+ }
6252
+ closeSync(fd);
6253
+ try {
6254
+ renameSync(tmp, target);
6255
+ } catch (e) {
6256
+ rmSync(tmp, { force: true });
6257
+ throw e;
5969
6258
  }
5970
- renameSync(tmp, path);
5971
6259
  }
5972
6260
 
5973
6261
  // src/cache.ts
@@ -6021,7 +6309,7 @@ function isCacheState(value) {
6021
6309
  function readState(source = "espn", competition = DEFAULT_COMPETITION) {
6022
6310
  try {
6023
6311
  const path = cachePath(source, competition);
6024
- const info = statSync(path);
6312
+ const info = statSync2(path);
6025
6313
  if (!info.isFile() || info.size > MAX_STATE_BYTES) return void 0;
6026
6314
  const bytes = readFileSync2(path);
6027
6315
  if (bytes.byteLength > MAX_STATE_BYTES) return void 0;
@@ -6075,7 +6363,7 @@ function lockAgeMs(now = Date.now()) {
6075
6363
  return Infinity;
6076
6364
  }
6077
6365
  try {
6078
- return stampAgeMs(new Date(statSync(lp).mtimeMs).toISOString(), now);
6366
+ return stampAgeMs(new Date(statSync2(lp).mtimeMs).toISOString(), now);
6079
6367
  } catch {
6080
6368
  return Infinity;
6081
6369
  }
@@ -6083,44 +6371,62 @@ function lockAgeMs(now = Date.now()) {
6083
6371
  function isLockFresh(now = Date.now()) {
6084
6372
  return lockAgeMs(now) < LOCK_STALE_MS;
6085
6373
  }
6086
- function acquireLock(now = Date.now()) {
6087
- mkdirSync2(cacheDir(), { recursive: true });
6088
- const lp = lockPath();
6374
+ var heldToken;
6375
+ function readLockToken() {
6376
+ try {
6377
+ return readFileSync2(lockPath(), "utf8").trim();
6378
+ } catch {
6379
+ return void 0;
6380
+ }
6381
+ }
6382
+ function writeExclusive(lp, token) {
6089
6383
  try {
6090
6384
  const fd = openSync2(lp, "wx");
6091
6385
  try {
6092
- writeSync2(fd, `${process.pid} ${now}`);
6386
+ writeSync2(fd, token);
6093
6387
  } finally {
6094
6388
  closeSync2(fd);
6095
6389
  }
6096
6390
  return true;
6097
6391
  } catch {
6098
- if (lockAgeMs(now) > LOCK_STALE_MS) {
6099
- try {
6100
- rmSync(lp, { force: true });
6101
- } catch {
6102
- return false;
6103
- }
6104
- try {
6105
- const fd = openSync2(lp, "wx");
6106
- try {
6107
- writeSync2(fd, `${process.pid} ${now}`);
6108
- } finally {
6109
- closeSync2(fd);
6110
- }
6111
- return true;
6112
- } catch {
6113
- return false;
6114
- }
6115
- }
6116
6392
  return false;
6117
6393
  }
6118
6394
  }
6119
- function releaseLock() {
6395
+ function claimLock(now = Date.now()) {
6396
+ mkdirSync2(cacheDir(), { recursive: true });
6397
+ const lp = lockPath();
6398
+ const token = `${process.pid} ${now} ${randomBytes2(6).toString("hex")}`;
6399
+ if (writeExclusive(lp, token)) return token;
6400
+ if (lockAgeMs(now) > LOCK_STALE_MS) {
6401
+ try {
6402
+ rmSync2(lp, { force: true });
6403
+ } catch {
6404
+ return void 0;
6405
+ }
6406
+ return writeExclusive(lp, token) ? token : void 0;
6407
+ }
6408
+ return void 0;
6409
+ }
6410
+ function holdsLock(token = heldToken) {
6411
+ return token !== void 0 && readLockToken() === token;
6412
+ }
6413
+ function acquireLock(now = Date.now()) {
6414
+ const token = claimLock(now);
6415
+ if (token) heldToken = token;
6416
+ return token !== void 0;
6417
+ }
6418
+ function releaseLock(token = heldToken) {
6419
+ if (!holdsLock(token)) return;
6120
6420
  try {
6121
- rmSync(lockPath(), { force: true });
6421
+ rmSync2(lockPath(), { force: true });
6122
6422
  } catch {
6123
6423
  }
6424
+ if (token === heldToken) heldToken = void 0;
6425
+ }
6426
+ function publishState(state, token = heldToken) {
6427
+ if (!holdsLock(token)) return false;
6428
+ writeState(state);
6429
+ return true;
6124
6430
  }
6125
6431
 
6126
6432
  // src/marketCache.ts
@@ -6134,7 +6440,7 @@ function cachePath2() {
6134
6440
  function readFile() {
6135
6441
  try {
6136
6442
  const path = cachePath2();
6137
- const info = statSync2(path);
6443
+ const info = statSync3(path);
6138
6444
  if (!info.isFile() || info.size > MAX_MARKET_CACHE_BYTES) return void 0;
6139
6445
  const bytes = readFileSync3(path);
6140
6446
  if (bytes.byteLength > MAX_MARKET_CACHE_BYTES) return void 0;
@@ -6308,7 +6614,7 @@ function isMatchShaped(m) {
6308
6614
  return !!x && typeof x === "object" && typeof x.id === "string" && typeof x.kickoff === "string" && !!x.home?.code && !!x.away?.code;
6309
6615
  }
6310
6616
  function nextOverall(now, fixtures = allFixtures()) {
6311
- return [...fixtures].sort(byKickoff).find((m) => Date.parse(m.kickoff) >= now && isResolvedFixture(m));
6617
+ return [...fixtures].sort(byKickoff).find((m) => isUpcoming(m, new Date(now)) && isResolvedFixture(m));
6312
6618
  }
6313
6619
  function teamTok(t2, flags) {
6314
6620
  return flags ? t2.flag : t2.code;
@@ -6412,7 +6718,8 @@ function renderPromptLine(state, opts = {}) {
6412
6718
  const live = liveList.items;
6413
6719
  const cachedFixtureList = sealFixtures(state?.fixtures);
6414
6720
  const cachedFixtures = [...cachedFixtureList.items];
6415
- const schedule = cachedFixtures.length ? mergeLive(allFixtures(), cachedFixtures) : void 0;
6721
+ const bundle = defaultCompetition ? allFixtures() : [];
6722
+ const schedule = cachedFixtures.length ? mergeLive(bundle, cachedFixtures) : defaultCompetition ? void 0 : [];
6416
6723
  if (team) {
6417
6724
  const mine = live.find((m) => m.home?.code === team || m.away?.code === team);
6418
6725
  if (mine) return `\u26BD ${matchSegment(mine, compact, flags)}`;
@@ -6456,14 +6763,15 @@ function boundContext(text, marker = "") {
6456
6763
  return `${points.slice(0, room).join("")}
6457
6764
  (context truncated)${marker}`;
6458
6765
  }
6459
- function rosterPinned(t2) {
6766
+ function rosterPinned(t2, pin) {
6767
+ if (!pin) return t2;
6460
6768
  const { team } = lookupTeam(t2.code);
6461
6769
  return team ? { ...t2, name: team.name, flag: team.flag } : t2;
6462
6770
  }
6463
- function line(m, flags) {
6771
+ function line(m, flags, pin) {
6464
6772
  const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
6465
- const h = rosterPinned(m.home);
6466
- const a = rosterPinned(m.away);
6773
+ const h = rosterPinned(m.home, pin);
6774
+ const a = rosterPinned(m.away, pin);
6467
6775
  const home = flags ? `${h.flag} ${h.name}` : h.name;
6468
6776
  const away = flags ? `${a.name} ${a.flag}` : a.name;
6469
6777
  return `${home} ${scoreline(m)} ${away} (${minute})`;
@@ -6472,6 +6780,7 @@ function renderHook(state, opts = {}) {
6472
6780
  const now = opts.now ?? /* @__PURE__ */ new Date();
6473
6781
  const team = opts.team?.toUpperCase();
6474
6782
  const flags = opts.flags ?? true;
6783
+ const pin = opts.defaultCompetition ?? true;
6475
6784
  const liveList = liveMatchesFromCache(state, now.getTime());
6476
6785
  let live = [...liveList.items];
6477
6786
  if (live.length === 0) return "";
@@ -6484,7 +6793,7 @@ function renderHook(state, opts = {}) {
6484
6793
  }
6485
6794
  const shown = live.slice(0, MAX_HOOK_MATCHES);
6486
6795
  const overflow = live.length - shown.length;
6487
- const lines = shown.map((mm) => line(mm, flags)).join("\n");
6796
+ const lines = shown.map((mm) => line(mm, flags, pin)).join("\n");
6488
6797
  const more = !liveList.complete ? "\n(more live matches may not be shown)" : overflow > 0 ? `
6489
6798
  (+${overflow} more not shown)` : "";
6490
6799
  return boundContext(`[Claudinho \u2014 live football scores right now]
@@ -6504,7 +6813,7 @@ function fixturesStale(state, now) {
6504
6813
  return fixturesAttemptAgeMs(state, now) > FIXTURES_EMPTY_TTL_MS;
6505
6814
  }
6506
6815
  function nextStaticUpcoming(nowMs) {
6507
- return [...allFixtures()].sort(byKickoff).find((m) => Date.parse(m.kickoff) >= nowMs);
6816
+ return [...allFixtures()].sort(byKickoff).find((m) => isUpcoming(m, new Date(nowMs)));
6508
6817
  }
6509
6818
  function inKnockoutPhase(nowMs) {
6510
6819
  if (resolveCompetition() !== DEFAULT_COMPETITION) return false;
@@ -6515,12 +6824,8 @@ function liveWindowActive(nowMs) {
6515
6824
  if (resolveCompetition() !== DEFAULT_COMPETITION) return true;
6516
6825
  return inLiveWindow(nowMs);
6517
6826
  }
6518
- function liveAdapter(source) {
6519
- const competition = resolveCompetition();
6520
- if (source === "espn" && competition === DEFAULT_COMPETITION) {
6521
- return new EspnAdapter({ enrichGroups: false });
6522
- }
6523
- return makeAdapter(source);
6827
+ function liveAdapter(source, now) {
6828
+ return makeAdapter(source, { enrichGroups: false, now });
6524
6829
  }
6525
6830
  async function runRefresh(opts = {}) {
6526
6831
  const now = opts.now ?? /* @__PURE__ */ new Date();
@@ -6566,7 +6871,8 @@ async function runRefresh(opts = {}) {
6566
6871
  }
6567
6872
  return;
6568
6873
  }
6569
- if (!acquireLock()) return;
6874
+ const token = claimLock();
6875
+ if (!token) return;
6570
6876
  try {
6571
6877
  let live = base?.live ?? [];
6572
6878
  let degraded = base?.degraded ?? false;
@@ -6575,7 +6881,9 @@ async function runRefresh(opts = {}) {
6575
6881
  let fixturesUpdatedAt = base?.fixturesUpdatedAt;
6576
6882
  let fixturesAttemptedAt = base?.fixturesAttemptedAt;
6577
6883
  let backoffUntil = base?.backoffUntil;
6578
- const adapter = liveAdapter(source);
6884
+ const realStart = Date.now();
6885
+ const clock = () => nowMs + (Date.now() - realStart);
6886
+ const adapter = liveAdapter(source, clock);
6579
6887
  if (needLive) {
6580
6888
  try {
6581
6889
  const r = await getLiveMatches(adapter, now);
@@ -6597,26 +6905,32 @@ async function runRefresh(opts = {}) {
6597
6905
  } catch {
6598
6906
  }
6599
6907
  }
6600
- if (adapter.lastError?.throttled) {
6601
- backoffUntil = new Date(
6602
- nowMs + BACKOFF_MS + Math.floor(Math.random() * BACKOFF_JITTER_MS)
6603
- ).toISOString();
6908
+ const armed = adapter.cooldownUntil;
6909
+ if (armed !== void 0 && armed > clock()) {
6910
+ const jitter = opts.jitterMs ?? Math.floor(Math.random() * BACKOFF_JITTER_MS);
6911
+ backoffUntil = new Date(Math.max(armed, nowMs + BACKOFF_MS) + jitter).toISOString();
6604
6912
  } else if (backoffUntil && Date.parse(backoffUntil) <= nowMs) {
6605
6913
  backoffUntil = void 0;
6606
6914
  }
6607
- writeState({
6608
- updatedAt,
6609
- live,
6610
- degraded,
6611
- source,
6612
- competition,
6613
- ...fixtures ? { fixtures } : {},
6614
- ...fixturesUpdatedAt ? { fixturesUpdatedAt } : {},
6615
- ...fixturesAttemptedAt ? { fixturesAttemptedAt } : {},
6616
- ...backoffUntil ? { backoffUntil } : {}
6617
- });
6915
+ const published = publishState(
6916
+ {
6917
+ updatedAt,
6918
+ live,
6919
+ degraded,
6920
+ source,
6921
+ competition,
6922
+ ...fixtures ? { fixtures } : {},
6923
+ ...fixturesUpdatedAt ? { fixturesUpdatedAt } : {},
6924
+ ...fixturesAttemptedAt ? { fixturesAttemptedAt } : {},
6925
+ ...backoffUntil ? { backoffUntil } : {}
6926
+ },
6927
+ token
6928
+ );
6929
+ if (!published && process.env.CLAUDINHO_DEBUG) {
6930
+ process.stderr.write("claudinho: refresh lease lost to a successor; snapshot not published\n");
6931
+ }
6618
6932
  } finally {
6619
- releaseLock();
6933
+ releaseLock(token);
6620
6934
  }
6621
6935
  }
6622
6936
  function shouldRefresh(now = Date.now(), state = readState("espn", resolveCompetition())) {
@@ -6648,7 +6962,7 @@ function spawnRefresh(source) {
6648
6962
  }
6649
6963
 
6650
6964
  // src/install.ts
6651
- import { copyFileSync, existsSync, readFileSync as readFileSync5 } from "fs";
6965
+ import { copyFileSync, existsSync, lstatSync as lstatSync2, readFileSync as readFileSync5 } from "fs";
6652
6966
  import { homedir as homedir2 } from "os";
6653
6967
  import { join as join5 } from "path";
6654
6968
  function claudeSettingsPath() {
@@ -6683,21 +6997,41 @@ function defaultStatusLineConfig(target, command) {
6683
6997
  function restartMessage(target) {
6684
6998
  return target === "cursor" ? "Restart Cursor CLI (or start a new session) to see it." : "Restart Claude Code to see it.";
6685
6999
  }
7000
+ function isSettingsObject(v) {
7001
+ return v !== null && typeof v === "object" && !Array.isArray(v);
7002
+ }
7003
+ function manual(path, snippet, why) {
7004
+ return { action: "manual", path, message: `${why} ${path}. Add this manually:
7005
+ ${snippet}` };
7006
+ }
6686
7007
  function readSettings(path, snippet) {
6687
- if (!existsSync(path)) return {};
7008
+ let entry;
6688
7009
  try {
6689
- return JSON.parse(readFileSync5(path, "utf8"));
7010
+ entry = lstatSync2(path);
6690
7011
  } catch {
6691
- return {
6692
- action: "manual",
6693
- path,
6694
- message: `Could not parse ${path}. Add this manually:
6695
- ${snippet}`
6696
- };
7012
+ return {};
7013
+ }
7014
+ if (entry.isSymbolicLink() && !existsSync(path)) {
7015
+ return manual(path, snippet, "Settings path is a symlink to a missing file:");
6697
7016
  }
7017
+ let parsed;
7018
+ try {
7019
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
7020
+ } catch {
7021
+ return manual(path, snippet, "Could not parse");
7022
+ }
7023
+ if (!isSettingsObject(parsed)) return manual(path, snippet, "Not a JSON settings object:");
7024
+ return parsed;
6698
7025
  }
6699
7026
  function isInitResult(v) {
6700
- return "action" in v && typeof v.action === "string";
7027
+ return typeof v.action === "string" && typeof v.path === "string" && typeof v.message === "string";
7028
+ }
7029
+ var SETTINGS_FILE_MODE = 384;
7030
+ var SETTINGS_WRITE = { mode: SETTINGS_FILE_MODE, followSymlinks: true };
7031
+ function validHookMatchers(slot) {
7032
+ return slot.every(
7033
+ (m) => isSettingsObject(m) && (m.hooks === void 0 || Array.isArray(m.hooks) && m.hooks.every(isSettingsObject))
7034
+ );
6701
7035
  }
6702
7036
  function initStatuslineFor(target, opts = {}) {
6703
7037
  const path = configPathFor(target, opts.path);
@@ -6712,12 +7046,12 @@ function initStatuslineFor(target, opts = {}) {
6712
7046
  const settings = parsed;
6713
7047
  const existing = settings.statusLine;
6714
7048
  if (isSameCommand(existing?.command, command)) {
6715
- const label = target === "cursor" ? "Cursor CLI statusline" : "Statusline";
6716
- return { action: "already", path, message: `${label} already configured (${path}).` };
7049
+ const label2 = target === "cursor" ? "Cursor CLI statusline" : "Statusline";
7050
+ return { action: "already", path, message: `${label2} already configured (${path}).` };
6717
7051
  }
6718
7052
  backupOnce(path);
6719
7053
  settings.statusLine = sl;
6720
- writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n");
7054
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n", SETTINGS_WRITE);
6721
7055
  const surface = target === "cursor" ? "Cursor CLI statusline" : "Statusline";
6722
7056
  return {
6723
7057
  action: "written",
@@ -6751,6 +7085,13 @@ function initHook(opts = {}) {
6751
7085
  const parsed = readSettings(path, snippet);
6752
7086
  if (isInitResult(parsed)) return parsed;
6753
7087
  const settings = parsed;
7088
+ if (settings.hooks !== void 0 && !isSettingsObject(settings.hooks)) {
7089
+ return manual(path, snippet, 'Unexpected "hooks" shape in');
7090
+ }
7091
+ const eventSlot = settings.hooks?.[CLAUDE_HOOK_EVENT];
7092
+ if (eventSlot !== void 0 && (!Array.isArray(eventSlot) || !validHookMatchers(eventSlot))) {
7093
+ return manual(path, snippet, `Unexpected "hooks.${CLAUDE_HOOK_EVENT}" shape in`);
7094
+ }
6754
7095
  if (claudeHookCommands(settings).some((c) => isSameCommand(c, command))) {
6755
7096
  return {
6756
7097
  action: "already",
@@ -6763,7 +7104,7 @@ function initHook(opts = {}) {
6763
7104
  const hooks = settings.hooks;
6764
7105
  hooks[CLAUDE_HOOK_EVENT] ??= [];
6765
7106
  hooks[CLAUDE_HOOK_EVENT].push({ hooks: [{ type: "command", command }] });
6766
- writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n");
7107
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n", SETTINGS_WRITE);
6767
7108
  return {
6768
7109
  action: "written",
6769
7110
  path,
@@ -6771,9 +7112,61 @@ function initHook(opts = {}) {
6771
7112
  };
6772
7113
  }
6773
7114
 
7115
+ // src/providerBackoff.ts
7116
+ function persistBackoff(source, competition, until, nowMs) {
7117
+ const token = claimLock(nowMs);
7118
+ if (!token) return false;
7119
+ try {
7120
+ const prior = readState(source, competition);
7121
+ const base = prior && prior.source === source && prior.competition === competition ? prior : {
7122
+ updatedAt: new Date(nowMs).toISOString(),
7123
+ live: [],
7124
+ degraded: true,
7125
+ // the provider refused us — never claim otherwise
7126
+ source,
7127
+ competition
7128
+ };
7129
+ return publishState({ ...base, backoffUntil: new Date(until).toISOString() }, token);
7130
+ } finally {
7131
+ releaseLock(token);
7132
+ }
7133
+ }
7134
+ function withPersistedBackoff(adapter, source, now = /* @__PURE__ */ new Date()) {
7135
+ const competition = resolveCompetition();
7136
+ const nowMs = now.getTime();
7137
+ const state = readCurrentState(source, competition);
7138
+ const accepted = state?.backoffUntil && backoffActive(state, nowMs) ? Date.parse(state.backoffUntil) : void 0;
7139
+ if (accepted !== void 0) adapter.armCooldown?.(accepted);
7140
+ let persisted = accepted;
7141
+ const persistIfNewer = (until) => {
7142
+ if (!(until > nowMs) || persisted !== void 0 && until <= persisted) return;
7143
+ if (persistBackoff(source, competition, until, nowMs)) persisted = until;
7144
+ };
7145
+ adapter.onCooldown?.(persistIfNewer);
7146
+ const afterCall = () => {
7147
+ const until = adapter.cooldownUntil;
7148
+ if (until !== void 0) persistIfNewer(until);
7149
+ };
7150
+ return new Proxy(adapter, {
7151
+ get(target, prop) {
7152
+ const value = Reflect.get(target, prop, target);
7153
+ if (typeof value === "function" && typeof prop === "string" && prop.startsWith("fetch")) {
7154
+ return async (...args) => {
7155
+ try {
7156
+ return await value.apply(target, args);
7157
+ } finally {
7158
+ afterCall();
7159
+ }
7160
+ };
7161
+ }
7162
+ return value;
7163
+ }
7164
+ });
7165
+ }
7166
+
6774
7167
  // src/commands.ts
6775
- function adapterFor({ cfg, adapter }) {
6776
- return adapter ?? makeAdapter(cfg.source);
7168
+ function adapterFor({ cfg, adapter, now }) {
7169
+ return withPersistedBackoff(adapter ?? makeAdapter(cfg.source), cfg.source, now);
6777
7170
  }
6778
7171
  var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
6779
7172
  var MARKETS_CMD_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
@@ -6952,20 +7345,30 @@ async function cmdNext(team, ctx) {
6952
7345
  const { cfg, t: t2, now } = ctx;
6953
7346
  precheck(cfg, t2);
6954
7347
  const code = resolveTeamArg(team, "Usage: claudinho next <team> (or set CLAUDINHO_TEAM)", t2);
6955
- const { fixture, degraded, source } = await getNextFixtureForTeam(
7348
+ const { fixture, degraded, source, unsupported } = await getNextFixtureForTeam(
6956
7349
  adapterFor(ctx),
6957
7350
  code,
6958
7351
  now ?? /* @__PURE__ */ new Date()
6959
7352
  );
6960
7353
  if (cfg.json) {
6961
- emitJson({ team: code, fixture: fixture ?? null, degraded, source: source ?? null });
7354
+ emitJson({
7355
+ team: code,
7356
+ fixture: fixture ?? null,
7357
+ degraded,
7358
+ source: source ?? null,
7359
+ ...unsupported ? { unsupported: true } : {}
7360
+ });
6962
7361
  return;
6963
7362
  }
6964
7363
  const c = painterFor(cfg);
6965
7364
  const flags = flagsEnabled();
6966
7365
  out();
6967
7366
  if (!fixture) {
6968
- out(c.dim(" " + (degraded ? t2("live.degraded") : t2("next.none", { team: code }))));
7367
+ out(
7368
+ c.dim(
7369
+ " " + (unsupported ? t2("competition.unsupported") : degraded ? t2("live.degraded") : t2("next.none", { team: code }))
7370
+ )
7371
+ );
6969
7372
  out();
6970
7373
  out(disclaimer(t2, c));
6971
7374
  endScoreCommand(ctx);
@@ -6997,7 +7400,7 @@ function cmdTeam(query, ctx) {
6997
7400
  }
6998
7401
  const c = painterFor(cfg);
6999
7402
  const flags = flagsEnabled();
7000
- const label = (tm) => {
7403
+ const label2 = (tm) => {
7001
7404
  const flag2 = flags ? `${tm.flag} ` : "";
7002
7405
  const grp = tm.group ? ` \xB7 ${t2("team.group", { group: tm.group })}` : "";
7003
7406
  return ` ${flag2}${c.bold(tm.name)} ${c.dim(tm.code + grp)}`;
@@ -7006,10 +7409,10 @@ function cmdTeam(query, ctx) {
7006
7409
  if (!q) {
7007
7410
  out(" " + c.dim(t2("team.usage")));
7008
7411
  } else if (team) {
7009
- out(label(team));
7412
+ out(label2(team));
7010
7413
  } else if (matches.length > 0) {
7011
7414
  out(" " + c.dim(t2("team.ambiguous", { query: q })));
7012
- for (const m of matches) out(label(m));
7415
+ for (const m of matches) out(label2(m));
7013
7416
  } else {
7014
7417
  out(" " + c.dim(t2("team.none", { query: q })));
7015
7418
  }
@@ -7022,7 +7425,11 @@ async function cmdTable(group, ctx) {
7022
7425
  precheck(cfg, t2);
7023
7426
  const { tables, degraded, source } = await getStandings(adapterFor(ctx), group);
7024
7427
  if (cfg.json) {
7025
- const json = tables.map((tb) => ({ group: tb.group, standings: tb.rows }));
7428
+ const json = tables.map((tb) => ({
7429
+ group: tb.group,
7430
+ standings: tb.rows,
7431
+ ...tb.partial ? { partial: tb.partial } : {}
7432
+ }));
7026
7433
  emitJson({
7027
7434
  degraded,
7028
7435
  source: source ?? null,
@@ -7045,7 +7452,7 @@ async function cmdTable(group, ctx) {
7045
7452
  out(disclaimer(t2, c));
7046
7453
  return;
7047
7454
  }
7048
- for (const { group: g, rows } of tables) {
7455
+ for (const { group: g, rows, partial } of tables) {
7049
7456
  out();
7050
7457
  out(header(t2("table.title", { group: g }), c));
7051
7458
  const table = new Table({
@@ -7073,6 +7480,7 @@ async function cmdTable(group, ctx) {
7073
7480
  ]);
7074
7481
  }
7075
7482
  out(table.toString());
7483
+ if (partial) out(c.dim(" " + t2("table.partial", { n: String(partial.omitted) })));
7076
7484
  }
7077
7485
  out();
7078
7486
  if (degraded) out(c.dim(" " + t2("table.degraded")));
@@ -7089,10 +7497,22 @@ async function cmdBracket(stage, opts, ctx) {
7089
7497
  if (filter && !BRACKET_STAGES.has(filter)) {
7090
7498
  throw new InputError(t(cfg.lang, "bracket.invalidStage"));
7091
7499
  }
7092
- const { view, degraded, standingsDegraded, source } = await getBracket(
7500
+ const { view, degraded, standingsDegraded, source, unsupported } = await getBracket(
7093
7501
  adapterFor(ctx),
7094
7502
  filter ? { stage: filter, lang: cfg.lang } : { lang: cfg.lang }
7095
7503
  );
7504
+ if (unsupported) {
7505
+ if (cfg.json) {
7506
+ emitJson({ degraded, standingsDegraded, source: null, view, unsupported: true });
7507
+ return;
7508
+ }
7509
+ const c2 = painterFor(cfg);
7510
+ out();
7511
+ out(c2.dim(` ${t(cfg.lang, "competition.unsupported")}`));
7512
+ out();
7513
+ out(disclaimer(t2, c2));
7514
+ return;
7515
+ }
7096
7516
  if (cfg.json) {
7097
7517
  emitJson({
7098
7518
  degraded,
@@ -7164,7 +7584,13 @@ function cmdHook({ cfg }) {
7164
7584
  try {
7165
7585
  const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
7166
7586
  const state = readCurrentState(cfg.source, resolveCompetition());
7167
- const ctx = renderHook(state, { team, flags: flagsEnabled() });
7587
+ const ctx = renderHook(state, {
7588
+ team,
7589
+ flags: flagsEnabled(),
7590
+ // The bundled roster names World Cup nations only; on another competition
7591
+ // a club sharing a nation's code must not be renamed to that nation.
7592
+ defaultCompetition: resolveCompetition() === DEFAULT_COMPETITION
7593
+ });
7168
7594
  if (ctx) out(ctx);
7169
7595
  if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
7170
7596
  spawnRefresh(cfg.source);
@@ -7251,7 +7677,7 @@ function cmdInitClaude(opts, { cfg }) {
7251
7677
  async function cmdMatch(id, ctx) {
7252
7678
  const { cfg, t: t2 } = ctx;
7253
7679
  precheck(cfg, t2);
7254
- const { match, degraded, source: liveSource } = await getMatchById(adapterFor(ctx), id);
7680
+ const { match, degraded, source: liveSource, unsupported } = await getMatchById(adapterFor(ctx), id);
7255
7681
  const market = match ? await reliableMarketSignalFor(ctx, match) : { signal: void 0, complete: true };
7256
7682
  if (cfg.json) {
7257
7683
  emitJson({
@@ -7259,14 +7685,15 @@ async function cmdMatch(id, ctx) {
7259
7685
  match: match ?? null,
7260
7686
  source: liveSource ?? null,
7261
7687
  marketComplete: market.complete,
7262
- marketSignal: market.signal ?? null
7688
+ marketSignal: market.signal ?? null,
7689
+ ...unsupported ? { unsupported: true } : {}
7263
7690
  });
7264
7691
  return;
7265
7692
  }
7266
7693
  const c = painterFor(cfg);
7267
7694
  out();
7268
7695
  if (!match) {
7269
- out(c.dim(" " + t2("match.none", { id })));
7696
+ out(c.dim(" " + (unsupported ? t2("competition.unsupported") : t2("match.none", { id }))));
7270
7697
  out();
7271
7698
  out(disclaimer(t2, c));
7272
7699
  return;
@@ -7303,6 +7730,7 @@ async function cmdMatch(id, ctx) {
7303
7730
  maybeStarNudge(ctx);
7304
7731
  }
7305
7732
  var MARKET_INFO = "Prediction-market data is informational only.";
7733
+ var MARKETS_SCOPE_NOTE = "Market signals cover the World Cup only; none are read for this competition.";
7306
7734
  function marketDisplayable(match, sig) {
7307
7735
  return marketSignalRendersFor(match, sig) && !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
7308
7736
  }
@@ -7311,6 +7739,7 @@ function marketHeaderLine(m, cfg) {
7311
7739
  return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag} \xB7 ${when}`;
7312
7740
  }
7313
7741
  function noSignalLine(m, now) {
7742
+ if (!marketsCoverCompetition()) return MARKETS_SCOPE_NOTE;
7314
7743
  if (marketRelevant(m, now)) return "No market signal for this match.";
7315
7744
  return isFinished(m.status) ? "Match has finished \u2014 market signals are pre-match and in-play reads." : "Match appears to have finished \u2014 market signals are pre-match and in-play reads.";
7316
7745
  }
@@ -7323,7 +7752,7 @@ async function cmdMarkets(target, team, ctx) {
7323
7752
  precheck(cfg, t2);
7324
7753
  const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)", t2);
7325
7754
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
7326
- const { match: fixture, degraded } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
7755
+ const { match: fixture, degraded, unsupported } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
7327
7756
  const market = fixture && marketRelevant(fixture, now2) ? await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
7328
7757
  const sig = fixture ? market.signals.get(fixture.id) : void 0;
7329
7758
  const shown = market.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
@@ -7334,14 +7763,21 @@ async function cmdMarkets(target, team, ctx) {
7334
7763
  degraded,
7335
7764
  informationalOnly: true,
7336
7765
  complete: market.complete,
7337
- signal: shown ?? null
7766
+ signal: shown ?? null,
7767
+ // Review P2 on #129: a JSON consumer must tell "not available for this
7768
+ // competition" from a successful empty result; the text branch already did.
7769
+ ...unsupported ? { unsupported: true } : {}
7338
7770
  });
7339
7771
  return;
7340
7772
  }
7341
7773
  const c2 = painterFor(cfg);
7342
7774
  out();
7343
7775
  if (!fixture) {
7344
- out(c2.dim(" " + (degraded ? t2("live.degraded") : t2("next.none", { team: code }))));
7776
+ out(
7777
+ c2.dim(
7778
+ " " + (unsupported ? t2("competition.unsupported") : degraded ? t2("live.degraded") : t2("next.none", { team: code }))
7779
+ )
7780
+ );
7345
7781
  } else {
7346
7782
  out(header(marketHeaderLine(fixture, cfg), c2));
7347
7783
  out();
@@ -7358,7 +7794,7 @@ async function cmdMarkets(target, team, ctx) {
7358
7794
  if (target && target !== "today" && !isValidDate(target)) {
7359
7795
  precheck(cfg, t2);
7360
7796
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
7361
- const { match } = await getMatchById(adapterFor(ctx), target);
7797
+ const { match, unsupported } = await getMatchById(adapterFor(ctx), target);
7362
7798
  const market = match && marketRelevant(match, now2) ? await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
7363
7799
  const sig = match ? market.signals.get(match.id) : void 0;
7364
7800
  const shown = market.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
@@ -7367,14 +7803,15 @@ async function cmdMarkets(target, team, ctx) {
7367
7803
  matchId: target,
7368
7804
  informationalOnly: true,
7369
7805
  complete: market.complete,
7370
- signal: shown ?? null
7806
+ signal: shown ?? null,
7807
+ ...unsupported ? { unsupported: true } : {}
7371
7808
  });
7372
7809
  return;
7373
7810
  }
7374
7811
  const c2 = painterFor(cfg);
7375
7812
  out();
7376
7813
  if (!match) {
7377
- out(c2.dim(" " + t2("match.none", { id: target })));
7814
+ out(c2.dim(" " + (unsupported ? t2("competition.unsupported") : t2("match.none", { id: target }))));
7378
7815
  } else {
7379
7816
  out(header(marketHeaderLine(match, cfg), c2));
7380
7817
  out();
@@ -7412,7 +7849,7 @@ async function cmdMarkets(target, team, ctx) {
7412
7849
  if (rows.length === 0) {
7413
7850
  out(
7414
7851
  c.dim(
7415
- complete ? ` No market signals available for ${date}.` : ` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
7852
+ !marketsCoverCompetition() ? ` ${MARKETS_SCOPE_NOTE}` : complete ? ` No market signals available for ${date}.` : ` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
7416
7853
  )
7417
7854
  );
7418
7855
  } else {
@@ -7455,7 +7892,9 @@ function emitShare(ctx, e, copy) {
7455
7892
  snippet,
7456
7893
  matches: e.input.matches,
7457
7894
  marketComplete: e.input.marketComplete ?? true,
7458
- marketSignals: Object.fromEntries(e.input.marketSignals ?? /* @__PURE__ */ new Map())
7895
+ marketSignals: Object.fromEntries(e.input.marketSignals ?? /* @__PURE__ */ new Map()),
7896
+ // The structured card keeps the verdict the snippet's note carries.
7897
+ ...e.unsupported ? { unsupported: true } : {}
7459
7898
  });
7460
7899
  } else {
7461
7900
  out(snippet);
@@ -7487,7 +7926,12 @@ function emitShareTable(ctx, e, copy) {
7487
7926
  degraded: e.degraded,
7488
7927
  informationalOnly: true,
7489
7928
  snippet,
7490
- tables: e.tables.map((tb) => ({ group: tb.group, standings: tb.rows }))
7929
+ // The structured card keeps the verdict the snippet warns about (A01).
7930
+ tables: e.tables.map((tb) => ({
7931
+ group: tb.group,
7932
+ standings: tb.rows,
7933
+ ...tb.partial ? { partial: tb.partial } : {}
7934
+ }))
7491
7935
  });
7492
7936
  } else {
7493
7937
  out(snippet);
@@ -7518,7 +7962,8 @@ function emitShareBracket(ctx, e, copy) {
7518
7962
  degraded: e.degraded,
7519
7963
  informationalOnly: true,
7520
7964
  snippet,
7521
- view: e.view
7965
+ view: e.view,
7966
+ ...e.unsupported ? { unsupported: true } : {}
7522
7967
  });
7523
7968
  } else {
7524
7969
  out(snippet);
@@ -7592,7 +8037,7 @@ async function cmdShare(target, team, opts, ctx) {
7592
8037
  if (stageFilter && !BRACKET_STAGES.has(stageFilter)) {
7593
8038
  throw new InputError(t(cfg.lang, "bracket.invalidStage"));
7594
8039
  }
7595
- const { view, degraded: degraded2, source: source2 } = await getBracket(
8040
+ const { view, degraded: degraded2, source: source2, unsupported } = await getBracket(
7596
8041
  adapterFor(ctx),
7597
8042
  stageFilter ? { stage: stageFilter, lang: cfg.lang } : { lang: cfg.lang }
7598
8043
  );
@@ -7604,7 +8049,8 @@ async function cmdShare(target, team, opts, ctx) {
7604
8049
  source: degraded2 ? void 0 : source2,
7605
8050
  degraded: degraded2,
7606
8051
  installLine: stageFilter ? `npx @claudinho/cli bracket ${stageFilter}` : "npx @claudinho/cli bracket",
7607
- emptyNote: t(cfg.lang, "bracket.empty"),
8052
+ emptyNote: unsupported ? t(cfg.lang, "competition.unsupported") : t(cfg.lang, "bracket.empty"),
8053
+ unsupported,
7608
8054
  options: {
7609
8055
  includeHashtag: baseOptions.includeHashtag,
7610
8056
  includeInstallLine: baseOptions.includeInstallLine,
@@ -7620,7 +8066,7 @@ async function cmdShare(target, team, opts, ctx) {
7620
8066
  if (target === "next") {
7621
8067
  precheck(cfg, t2);
7622
8068
  const code = resolveTeamArg(team, "Usage: claudinho share next <team> (or set CLAUDINHO_TEAM)", t2);
7623
- const { fixture, degraded: degraded2, source: source2 } = await getNextFixtureForTeam(
8069
+ const { fixture, degraded: degraded2, source: source2, unsupported } = await getNextFixtureForTeam(
7624
8070
  adapterFor(ctx),
7625
8071
  code,
7626
8072
  ctx.now ?? /* @__PURE__ */ new Date()
@@ -7644,12 +8090,13 @@ async function cmdShare(target, team, opts, ctx) {
7644
8090
  source: source2,
7645
8091
  degraded: degraded2,
7646
8092
  // Fail-closed: an outage must never paste as "no fixture" (eliminated).
7647
- emptyNote: degraded2 ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`,
8093
+ emptyNote: unsupported ? t(cfg.lang, "competition.unsupported") : degraded2 ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`,
7648
8094
  installLine: `npx @claudinho/cli next ${code}`,
7649
8095
  tz: cfg.tz,
7650
8096
  locale: cfg.lang
7651
8097
  },
7652
- options: baseOptions
8098
+ options: baseOptions,
8099
+ unsupported
7653
8100
  },
7654
8101
  copy
7655
8102
  );
@@ -7657,7 +8104,7 @@ async function cmdShare(target, team, opts, ctx) {
7657
8104
  }
7658
8105
  if (target && target !== "today" && !isValidDate(target)) {
7659
8106
  precheck(cfg, t2);
7660
- const { match, degraded: degraded2, source: source2 } = await getMatchById(adapterFor(ctx), target);
8107
+ const { match, degraded: degraded2, source: source2, unsupported } = await getMatchById(adapterFor(ctx), target);
7661
8108
  const matches = match ? [match] : [];
7662
8109
  const market2 = await reliableShareSignals(ctx, matches);
7663
8110
  emitShare(
@@ -7672,12 +8119,13 @@ async function cmdShare(target, team, opts, ctx) {
7672
8119
  marketComplete: market2.complete,
7673
8120
  source: source2,
7674
8121
  degraded: degraded2,
7675
- emptyNote: `No match found with id ${target}.`,
8122
+ emptyNote: unsupported ? t(cfg.lang, "competition.unsupported") : `No match found with id ${target}.`,
7676
8123
  installLine: `npx @claudinho/cli match ${target}`,
7677
8124
  tz: cfg.tz,
7678
8125
  locale: cfg.lang
7679
8126
  },
7680
- options: baseOptions
8127
+ options: baseOptions,
8128
+ unsupported
7681
8129
  },
7682
8130
  copy
7683
8131
  );
@@ -7829,7 +8277,7 @@ function handlePipeError(stream) {
7829
8277
  }
7830
8278
  handlePipeError(process.stdout);
7831
8279
  handlePipeError(process.stderr);
7832
- var VERSION = "0.9.4";
8280
+ var VERSION = "0.10.1";
7833
8281
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
7834
8282
  function ctxFrom(cmd) {
7835
8283
  let root = cmd;