@claudinho/cli 0.8.18 โ†’ 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +495 -210
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -186,7 +186,7 @@ The statusline reads from a local micro-cache and **never blocks on the
186
186
  network** (<150ms). When several matches are live it shows them all inline:
187
187
  `โšฝ ๐Ÿ‡ณ๐Ÿ‡ด 1โ€“1 ๐Ÿ‡ซ๐Ÿ‡ท 87' ยท ๐Ÿ‡ธ๐Ÿ‡ณ 1โ€“2 ๐Ÿ‡ฎ๐Ÿ‡ถ 86'`. Customize via env:
188
188
 
189
- - `CLAUDINHO_TEAM=MEX` โ€” show only your team's match; also the default team for `next`, `markets next`, and `share next` when the argument is omitted
189
+ - `CLAUDINHO_TEAM=MEX` โ€” show only your team's match (a nation name works too, e.g. `CLAUDINHO_TEAM=mexico`); also the default team for `next`, `markets next`, and `share next` when the argument is omitted
190
190
  - `CLAUDINHO_MAX=2` โ€” cap how many live matches show inline (rest collapse to `+N`; default: all)
191
191
  - `CLAUDINHO_COMPACT=0` โ€” show 3-letter codes alongside flags
192
192
  - `CLAUDINHO_FLAGS=off` โ€” drop emoji flags for 3-letter codes (statusline) / plain names (`today`, `live`, `table`, `next`, hook); already automatic on terminals that can't render flag emoji, e.g. Warp
package/dist/index.js CHANGED
@@ -381,7 +381,8 @@ function normalizeLang(lang) {
381
381
  }
382
382
  function t(lang, key, vars) {
383
383
  const dict = CATALOGS[normalizeLang(lang)];
384
- let s = dict[key] ?? EN[key] ?? key;
384
+ const en = EN;
385
+ let s = dict[key] ?? en[key] ?? key;
385
386
  if (vars) {
386
387
  for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
387
388
  }
@@ -500,6 +501,63 @@ function shiftUtcDate(dateISO, days) {
500
501
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
501
502
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
502
503
  }
504
+ var FEED_TEXT_MAX = 100;
505
+ function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
506
+ let out2 = "";
507
+ let count = 0;
508
+ for (const ch of String(value)) {
509
+ const cp = ch.codePointAt(0) ?? 0;
510
+ const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
511
+ if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
512
+ if (count >= max) break;
513
+ out2 += isWhitespaceControl ? " " : ch;
514
+ count++;
515
+ }
516
+ return out2;
517
+ }
518
+ function sanitizeTeam(t2) {
519
+ return {
520
+ ...t2 ?? {},
521
+ code: sanitizeFeedText(t2?.code ?? ""),
522
+ name: sanitizeFeedText(t2?.name ?? ""),
523
+ flag: sanitizeFeedText(t2?.flag ?? "")
524
+ };
525
+ }
526
+ function finiteOrUndefined(v) {
527
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
528
+ }
529
+ function sanitizeScorePair(v) {
530
+ const home = finiteOrUndefined(v?.home);
531
+ const away = finiteOrUndefined(v?.away);
532
+ return home !== void 0 && away !== void 0 ? { home, away } : void 0;
533
+ }
534
+ function sanitizeMatchStrings(m) {
535
+ const score = sanitizeScorePair(m.score);
536
+ return {
537
+ ...m,
538
+ venue: sanitizeFeedText(m.venue ?? ""),
539
+ city: m.city == null ? m.city : sanitizeFeedText(m.city),
540
+ country: m.country == null ? m.country : sanitizeFeedText(m.country),
541
+ home: sanitizeTeam(m.home),
542
+ away: sanitizeTeam(m.away),
543
+ score,
544
+ shootout: score ? sanitizeScorePair(m.shootout) : void 0,
545
+ minute: finiteOrUndefined(m.minute)
546
+ };
547
+ }
548
+ var segmenter = new Intl.Segmenter();
549
+ var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
550
+ function displayWidth(s) {
551
+ let w = 0;
552
+ for (const { segment } of segmenter.segment(s)) {
553
+ w += WIDE_CLUSTER.test(segment) ? 2 : 1;
554
+ }
555
+ return w;
556
+ }
557
+ function padVisible(s, width) {
558
+ const w = displayWidth(s);
559
+ return w >= width ? s : s + " ".repeat(width - w);
560
+ }
503
561
  function outcomeFromScore(home, away) {
504
562
  if (home > away) return "H";
505
563
  if (home < away) return "A";
@@ -2886,10 +2944,27 @@ function rosterAtZero(matches) {
2886
2944
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2887
2945
  var DEFAULT_COMPETITION = "fifa.world";
2888
2946
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2889
- var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
2947
+ var USER_AGENT = `claudinho/${"0.9.0"} (+https://github.com/arturogarrido/claudinho)`;
2948
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2890
2949
  function competitionBase(slug) {
2891
2950
  return `${ESPN_SOCCER}/${slug}`;
2892
2951
  }
2952
+ var DEFAULT_TIMEOUT_MS = 6e3;
2953
+ var STANDINGS_SHARE_MS = 3e4;
2954
+ var ProviderError = class extends Error {
2955
+ kind;
2956
+ status;
2957
+ constructor(message, kind, status) {
2958
+ super(message);
2959
+ this.name = "ProviderError";
2960
+ this.kind = kind;
2961
+ this.status = status;
2962
+ }
2963
+ /** 429/403 โ€” the upstream is refusing us; retrying at the live cadence makes it worse. */
2964
+ get throttled() {
2965
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
2966
+ }
2967
+ };
2893
2968
  function mapStatus(st) {
2894
2969
  const name = (st?.type?.name ?? "").toUpperCase();
2895
2970
  const state = st?.type?.state ?? "";
@@ -2930,9 +3005,15 @@ function toInt(s) {
2930
3005
  return Number.isFinite(n) ? n : void 0;
2931
3006
  }
2932
3007
  function toTeam(t2) {
2933
- const name = t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD";
2934
- const code = (t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
2935
- return { code, name, flag: nationToFlag(t2?.displayName ?? t2?.abbreviation ?? name) };
3008
+ const name = sanitizeFeedText(
3009
+ t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
3010
+ );
3011
+ const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3012
+ return {
3013
+ code,
3014
+ name,
3015
+ flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
3016
+ };
2936
3017
  }
2937
3018
  function mapEspnEvent(ev, ctx = {}) {
2938
3019
  const comp = ev.competitions?.[0];
@@ -2963,9 +3044,9 @@ function mapEspnEvent(ev, ctx = {}) {
2963
3044
  stage,
2964
3045
  group,
2965
3046
  kickoff: ev.date,
2966
- venue: comp?.venue?.fullName ?? "",
2967
- city: comp?.venue?.address?.city || void 0,
2968
- country: comp?.venue?.address?.country || void 0,
3047
+ venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3048
+ city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3049
+ country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
2969
3050
  home,
2970
3051
  away,
2971
3052
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3025,6 +3106,20 @@ var EspnAdapter = class {
3025
3106
  capabilities = { push: false, latencyHintSec: 45 };
3026
3107
  /** Cached team-code -> group-letter map (built lazily from standings). */
3027
3108
  groupMap;
3109
+ /**
3110
+ * One in-flight/recent standings fetch shared by fetchStandings and
3111
+ * fetchGroupMap, so a command like `bracket` hits the endpoint once instead
3112
+ * of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
3113
+ * fetch clears the slot โ€” a transient failure is never cached.
3114
+ */
3115
+ standingsShared;
3116
+ /**
3117
+ * The most recent request failure (best-effort under concurrency; cleared
3118
+ * when a new request starts). A post-hoc hint for callers whose result path
3119
+ * fails closed to `degraded` booleans but who still need to distinguish a
3120
+ * throttle (persist a backoff) from an ordinary blip.
3121
+ */
3122
+ lastError;
3028
3123
  async fetchByDate(dateISO) {
3029
3124
  return this.fetchScoreboard(toEspnDate(dateISO));
3030
3125
  }
@@ -3046,37 +3141,58 @@ var EspnAdapter = class {
3046
3141
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3047
3142
  return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
3048
3143
  }
3144
+ /** The shared standings fetch (see {@link standingsShared}). */
3145
+ sharedStandings() {
3146
+ const now = Date.now();
3147
+ if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3148
+ return this.standingsShared.promise;
3149
+ }
3150
+ const promise = this.get(this.standingsUrl()).then(
3151
+ (d) => parseStandings(d)
3152
+ );
3153
+ this.standingsShared = { at: now, promise };
3154
+ promise.catch(() => {
3155
+ if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3156
+ });
3157
+ return promise;
3158
+ }
3049
3159
  /**
3050
3160
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3051
3161
  * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3052
3162
  * non-group `children` are filtered out by {@link parseStandings}.
3053
3163
  */
3054
3164
  async fetchStandings() {
3055
- return parseStandings(await this.get(this.standingsUrl()));
3165
+ return this.sharedStandings();
3056
3166
  }
3057
3167
  /**
3058
3168
  * Build (and cache) a team-code -> group-letter map from the standings
3059
- * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
3060
- * same parse as {@link fetchStandings}, so the two never drift.
3169
+ * endpoint. Best-effort: returns {} if standings are unavailable โ€” but a
3170
+ * transient failure is NOT cached (only a successful parse pins the map), so
3171
+ * one blip can't silently drop group letters for the adapter's lifetime.
3172
+ * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3173
+ * drift and one command never fetches standings twice.
3061
3174
  */
3062
3175
  async fetchGroupMap(force = false) {
3063
3176
  if (this.groupMap && !force) return this.groupMap;
3064
- const map = {};
3065
3177
  try {
3066
- const tables = parseStandings(await this.get(this.standingsUrl()));
3178
+ const tables = await this.sharedStandings();
3179
+ const map = {};
3067
3180
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3181
+ this.groupMap = map;
3182
+ return map;
3068
3183
  } catch {
3184
+ return {};
3069
3185
  }
3070
- this.groupMap = map;
3071
- return map;
3072
3186
  }
3073
3187
  async fetchScoreboard(dates) {
3074
3188
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3075
3189
  const url = new URL(`${base}/scoreboard`);
3076
3190
  url.searchParams.set("limit", "300");
3077
3191
  if (dates) url.searchParams.set("dates", dates);
3078
- const groupByTeam = this.opts.enrichGroups === false ? {} : await this.fetchGroupMap();
3079
- const data = await this.get(url.toString());
3192
+ const [groupByTeam, data] = await Promise.all([
3193
+ this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3194
+ this.get(url.toString())
3195
+ ]);
3080
3196
  return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3081
3197
  }
3082
3198
  async get(url) {
@@ -3084,17 +3200,51 @@ var EspnAdapter = class {
3084
3200
  const controller = new AbortController();
3085
3201
  const timer = setTimeout(
3086
3202
  () => controller.abort(),
3087
- this.opts.timeoutMs ?? 15e3
3203
+ this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
3088
3204
  );
3205
+ this.lastError = void 0;
3089
3206
  try {
3090
- const res = await doFetch(url, {
3091
- signal: controller.signal,
3092
- headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3093
- });
3207
+ let res;
3208
+ try {
3209
+ res = await doFetch(url, {
3210
+ signal: controller.signal,
3211
+ // ESPN never legitimately redirects; following one would sidestep the
3212
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3213
+ redirect: "error",
3214
+ headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3215
+ });
3216
+ } catch (e) {
3217
+ throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3218
+ }
3094
3219
  if (!res.ok) {
3095
- throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3220
+ throw new ProviderError(
3221
+ `ESPN request failed: ${res.status} ${res.statusText}`,
3222
+ "http",
3223
+ res.status
3224
+ );
3225
+ }
3226
+ const length = Number(res.headers?.get?.("content-length"));
3227
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3228
+ throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
3096
3229
  }
3097
- return await res.json();
3230
+ try {
3231
+ return await res.json();
3232
+ } catch (e) {
3233
+ throw new ProviderError(
3234
+ `ESPN response unparseable: ${e?.message ?? e}`,
3235
+ "parse"
3236
+ );
3237
+ }
3238
+ } catch (e) {
3239
+ const pe = e instanceof ProviderError ? e : new ProviderError(String(e?.message ?? e), "http");
3240
+ this.lastError = pe;
3241
+ if (typeof process !== "undefined" && process.env?.CLAUDINHO_DEBUG) {
3242
+ process.stderr.write(
3243
+ `claudinho: espn ${pe.kind}${pe.status ? ` ${pe.status}` : ""}: ${url}
3244
+ `
3245
+ );
3246
+ }
3247
+ throw pe;
3098
3248
  } finally {
3099
3249
  clearTimeout(timer);
3100
3250
  }
@@ -3818,13 +3968,18 @@ function resolveCompetition(explicit) {
3818
3968
  }
3819
3969
  return DEFAULT_COMPETITION;
3820
3970
  }
3971
+ var KNOWN_SOURCES = ["espn"];
3821
3972
  function makeAdapter(source = "espn") {
3822
3973
  switch (source) {
3823
- default: {
3974
+ case "espn": {
3824
3975
  const competition = resolveCompetition();
3825
3976
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
3826
3977
  return new EspnAdapter({ baseUrl });
3827
3978
  }
3979
+ default:
3980
+ throw new Error(
3981
+ `Unknown data source "${source}" (available: ${KNOWN_SOURCES.join(", ")})`
3982
+ );
3828
3983
  }
3829
3984
  }
3830
3985
  function mergeLive(base, live) {
@@ -3862,8 +4017,16 @@ async function getStandings(adapter, group) {
3862
4017
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
3863
4018
  return { tables, degraded: true };
3864
4019
  }
3865
- var KNOCKOUT_WINDOW_START = "20260628";
3866
- var KNOCKOUT_WINDOW_END = "20260719";
4020
+ var knockoutWindowMemo;
4021
+ function knockoutWindow() {
4022
+ if (knockoutWindowMemo !== void 0) return knockoutWindowMemo;
4023
+ const days = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY").map((m) => m.kickoff.slice(0, 10).replace(/-/g, "")).filter((d) => d.length === 8);
4024
+ knockoutWindowMemo = days.length ? {
4025
+ start: days.reduce((a, b) => a < b ? a : b),
4026
+ end: days.reduce((a, b) => a > b ? a : b)
4027
+ } : null;
4028
+ return knockoutWindowMemo;
4029
+ }
3867
4030
  async function getBracket(adapter, opts = {}) {
3868
4031
  const topology = loadBracketTopology();
3869
4032
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
@@ -3871,7 +4034,8 @@ async function getBracket(adapter, opts = {}) {
3871
4034
  let liveDegraded = true;
3872
4035
  let source;
3873
4036
  try {
3874
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4037
+ const win = knockoutWindow();
4038
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
3875
4039
  matches = mergeLive(base, live);
3876
4040
  liveDegraded = false;
3877
4041
  source = adapter.name;
@@ -3904,8 +4068,9 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
3904
4068
  let fixtures = allFixtures();
3905
4069
  let overlayFailed = false;
3906
4070
  try {
3907
- if (adapter.fetchWindow) {
3908
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END));
4071
+ const win = knockoutWindow();
4072
+ if (adapter.fetchWindow && win) {
4073
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
3909
4074
  }
3910
4075
  } catch {
3911
4076
  overlayFailed = true;
@@ -3928,7 +4093,8 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
3928
4093
  let degraded = true;
3929
4094
  let liveById;
3930
4095
  try {
3931
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4096
+ const win = knockoutWindow();
4097
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
3932
4098
  matches = mergeLive(base, live);
3933
4099
  degraded = false;
3934
4100
  liveById = new Set(live.map((m) => m.id));
@@ -3939,10 +4105,11 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
3939
4105
  return { fixture, degraded, source };
3940
4106
  }
3941
4107
  async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
3942
- if (!adapter.fetchWindow) return { fixtures: [], degraded: true };
4108
+ const win = knockoutWindow();
4109
+ if (!adapter.fetchWindow || !win) return { fixtures: [], degraded: true };
3943
4110
  let live;
3944
4111
  try {
3945
- live = await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END);
4112
+ live = await adapter.fetchWindow(win.start, win.end);
3946
4113
  } catch {
3947
4114
  return { fixtures: [], degraded: true };
3948
4115
  }
@@ -4190,7 +4357,7 @@ var mapping_2026_default = {
4190
4357
  var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4191
4358
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4192
4359
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4193
- var DEFAULT_TIMEOUT_MS = 8e3;
4360
+ var DEFAULT_TIMEOUT_MS2 = 8e3;
4194
4361
  var WC_SERIES_SLUG = "soccer-fifwc";
4195
4362
  var WC_SPORT = "fifwc";
4196
4363
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4228,7 +4395,7 @@ var PolymarketProvider = class {
4228
4395
  const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4229
4396
  const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4230
4397
  if (slugs.length === 0) return { checked: true };
4231
- const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4398
+ const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4232
4399
  try {
4233
4400
  for (const slug of slugs) {
4234
4401
  const remaining = deadline - Date.now();
@@ -4248,13 +4415,20 @@ var PolymarketProvider = class {
4248
4415
  const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
4249
4416
  const doFetch = this.opts.fetchImpl ?? fetch;
4250
4417
  const res = await doFetch(url, {
4251
- signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4418
+ signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
4419
+ // Gamma never legitimately redirects; following one would sidestep the
4420
+ // host allow-list (it only validates the base URL), so reject redirects.
4421
+ redirect: "error",
4252
4422
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4253
4423
  });
4254
4424
  if (res.status === 404) return void 0;
4255
4425
  if (!res.ok) {
4256
4426
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4257
4427
  }
4428
+ const length = Number(res.headers?.get?.("content-length"));
4429
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4430
+ throw new Error(`Polymarket response too large: ${length} bytes`);
4431
+ }
4258
4432
  const data = await res.json();
4259
4433
  const event = Array.isArray(data) ? data[0] : data;
4260
4434
  return event && typeof event === "object" ? event : void 0;
@@ -4732,6 +4906,82 @@ function resolveConfig(opts) {
4732
4906
  };
4733
4907
  }
4734
4908
 
4909
+ // src/cursorPayload.ts
4910
+ import { readFileSync } from "fs";
4911
+ function parseCursorPayload(raw) {
4912
+ try {
4913
+ const trimmed = raw.trim();
4914
+ if (!trimmed) return void 0;
4915
+ return JSON.parse(trimmed);
4916
+ } catch {
4917
+ return void 0;
4918
+ }
4919
+ }
4920
+ function readCursorPayload() {
4921
+ try {
4922
+ if (process.stdin.isTTY) return void 0;
4923
+ return parseCursorPayload(readFileSync(0, "utf8"));
4924
+ } catch {
4925
+ return void 0;
4926
+ }
4927
+ }
4928
+ function readCursorPayloadBounded(timeoutMs = 100, stdin = process.stdin) {
4929
+ if (stdin.isTTY) return Promise.resolve(void 0);
4930
+ return new Promise((resolve) => {
4931
+ const chunks = [];
4932
+ const done = () => {
4933
+ clearTimeout(timer);
4934
+ stdin.off("data", onData);
4935
+ stdin.off("end", done);
4936
+ stdin.off("error", done);
4937
+ stdin.pause();
4938
+ resolve(parseCursorPayload(Buffer.concat(chunks).toString("utf8")));
4939
+ };
4940
+ const onData = (c) => {
4941
+ chunks.push(c);
4942
+ };
4943
+ const timer = setTimeout(done, timeoutMs);
4944
+ timer.unref?.();
4945
+ stdin.on("data", onData);
4946
+ stdin.once("end", done);
4947
+ stdin.once("error", done);
4948
+ stdin.resume();
4949
+ });
4950
+ }
4951
+ function looksLikeCursorPayload(payload) {
4952
+ return !!(payload.model?.display_name || payload.context_window != null || payload.render_width_chars != null || payload.worktree?.name || payload.vim?.mode);
4953
+ }
4954
+ function cursorMetaEnabled(payload) {
4955
+ const v = (process.env.CLAUDINHO_CURSOR_META ?? "").toLowerCase();
4956
+ if (v === "0" || v === "false" || v === "no") return false;
4957
+ if (v === "1" || v === "true" || v === "yes") return !!payload;
4958
+ if (v === "auto") return !!payload && looksLikeCursorPayload(payload);
4959
+ return false;
4960
+ }
4961
+ function renderCursorMetaLine(payload) {
4962
+ const parts = [];
4963
+ const model = payload.model?.display_name;
4964
+ if (model) {
4965
+ let label = model;
4966
+ if (payload.model?.param_summary) label += ` ${payload.model.param_summary}`;
4967
+ parts.push(label);
4968
+ }
4969
+ const pct2 = payload.context_window?.used_percentage;
4970
+ if (typeof pct2 === "number" && Number.isFinite(pct2)) {
4971
+ parts.push(`ctx ${Math.floor(pct2)}%`);
4972
+ }
4973
+ if (payload.worktree?.name) parts.push(`wt ${payload.worktree.name}`);
4974
+ if (payload.vim?.mode) parts.push(payload.vim.mode);
4975
+ if (parts.length === 0) return void 0;
4976
+ return `\x1B[90m${parts.join(" ")}\x1B[0m`;
4977
+ }
4978
+ function renderPromptOutput(scoreLine, payload) {
4979
+ const meta = payload && cursorMetaEnabled(payload) ? renderCursorMetaLine(payload) : void 0;
4980
+ if (meta) return `${scoreLine}
4981
+ ${meta}`;
4982
+ return scoreLine;
4983
+ }
4984
+
4735
4985
  // src/i18n.ts
4736
4986
  var EN2 = {
4737
4987
  "today.title": "Today's matches",
@@ -4753,7 +5003,6 @@ var EN2 = {
4753
5003
  "table.degraded": "Live standings unavailable \u2014 showing the group roster.",
4754
5004
  "table.empty": "No standings available.",
4755
5005
  "match.none": "No match found with id {id}.",
4756
- "status.scheduled": "scheduled",
4757
5006
  "status.live": "LIVE",
4758
5007
  "status.ht": "HT",
4759
5008
  "status.ft": "FT",
@@ -4767,6 +5016,7 @@ var EN2 = {
4767
5016
  "col.gd": "GD",
4768
5017
  "col.pts": "Pts",
4769
5018
  "err.date": "Invalid date {date}. Use YYYY-MM-DD.",
5019
+ "err.source": "Unknown data source {source}. Available: {sources}.",
4770
5020
  "warn.tz": "Unknown timezone {tz}; using system timezone.",
4771
5021
  "warn.lang": "Unsupported language {lang}; using English. (supported: en, es, pt, fr)",
4772
5022
  disclaimer: "Not affiliated with FIFA or Anthropic."
@@ -4791,7 +5041,6 @@ var ES2 = {
4791
5041
  "table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
4792
5042
  "table.empty": "No hay clasificaci\xF3n disponible.",
4793
5043
  "match.none": "No se encontr\xF3 partido con id {id}.",
4794
- "status.scheduled": "programado",
4795
5044
  "status.live": "EN VIVO",
4796
5045
  "status.ht": "DESC",
4797
5046
  "status.ft": "FIN",
@@ -4805,6 +5054,7 @@ var ES2 = {
4805
5054
  "col.gd": "DG",
4806
5055
  "col.pts": "Pts",
4807
5056
  "err.date": "Fecha inv\xE1lida {date}. Usa AAAA-MM-DD.",
5057
+ "err.source": "Fuente de datos desconocida {source}. Disponibles: {sources}.",
4808
5058
  "warn.tz": "Zona horaria desconocida {tz}; usando la del sistema.",
4809
5059
  "warn.lang": "Idioma no soportado {lang}; usando ingl\xE9s. (disponibles: en, es, pt, fr)",
4810
5060
  disclaimer: "No afiliado a FIFA ni Anthropic."
@@ -4829,7 +5079,6 @@ var PT2 = {
4829
5079
  "table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
4830
5080
  "table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
4831
5081
  "match.none": "Nenhum jogo encontrado com id {id}.",
4832
- "status.scheduled": "agendado",
4833
5082
  "status.live": "AO VIVO",
4834
5083
  "status.ht": "INT",
4835
5084
  "status.ft": "FIM",
@@ -4843,6 +5092,7 @@ var PT2 = {
4843
5092
  "col.gd": "SG",
4844
5093
  "col.pts": "Pts",
4845
5094
  "err.date": "Data inv\xE1lida {date}. Use AAAA-MM-DD.",
5095
+ "err.source": "Fonte de dados desconhecida {source}. Dispon\xEDveis: {sources}.",
4846
5096
  "warn.tz": "Fuso hor\xE1rio desconhecido {tz}; usando o do sistema.",
4847
5097
  "warn.lang": "Idioma n\xE3o suportado {lang}; usando ingl\xEAs. (dispon\xEDveis: en, es, pt, fr)",
4848
5098
  disclaimer: "N\xE3o afiliado \xE0 FIFA nem \xE0 Anthropic."
@@ -4867,7 +5117,6 @@ var FR2 = {
4867
5117
  "table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
4868
5118
  "table.empty": "Aucun classement disponible.",
4869
5119
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
4870
- "status.scheduled": "pr\xE9vu",
4871
5120
  "status.live": "DIRECT",
4872
5121
  "status.ht": "MT",
4873
5122
  "status.ft": "FIN",
@@ -4881,6 +5130,7 @@ var FR2 = {
4881
5130
  "col.gd": "Diff",
4882
5131
  "col.pts": "Pts",
4883
5132
  "err.date": "Date invalide {date}. Utilisez AAAA-MM-JJ.",
5133
+ "err.source": "Source de donn\xE9es inconnue {source}. Disponibles : {sources}.",
4884
5134
  "warn.tz": "Fuseau horaire inconnu {tz} ; utilisation du fuseau syst\xE8me.",
4885
5135
  "warn.lang": "Langue non prise en charge {lang} ; utilisation de l\u2019anglais. (disponibles : en, es, pt, fr)",
4886
5136
  disclaimer: "Non affili\xE9 \xE0 la FIFA ni \xE0 Anthropic."
@@ -4888,9 +5138,10 @@ var FR2 = {
4888
5138
  var CATALOGS2 = { en: EN2, es: ES2, pt: PT2, fr: FR2 };
4889
5139
  function makeT(lang) {
4890
5140
  const dict = CATALOGS2[lang] ?? EN2;
5141
+ const en = EN2;
4891
5142
  return (key, vars) => {
4892
- let s = dict[key] ?? EN2[key] ?? key;
4893
- if (vars) for (const [k, v] of Object.entries(vars)) s = s.replace(`{${k}}`, v);
5143
+ let s = dict[key] ?? en[key] ?? key;
5144
+ if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
4894
5145
  return s;
4895
5146
  };
4896
5147
  }
@@ -4949,7 +5200,7 @@ function matchLine(m, cfg, t2, c, flags = true) {
4949
5200
  const home = flags ? `${m.home.flag} ${m.home.name}` : m.home.name;
4950
5201
  const away = flags ? `${m.away.name} ${m.away.flag}` : m.away.name;
4951
5202
  const mid2 = isLive(m.status) || m.status === "FT" ? c.bold(scoreline(m)) : c.dim("vs");
4952
- const left = `${home.padEnd(22)} ${mid2.padStart(3)} ${away}`;
5203
+ const left = `${padVisible(home, 22)} ${mid2.padStart(3)} ${away}`;
4953
5204
  let right = "";
4954
5205
  if (m.status === "SCHEDULED") {
4955
5206
  right = c.dim(
@@ -4974,21 +5225,38 @@ function dataSource(source, lang, c) {
4974
5225
  }
4975
5226
 
4976
5227
  // src/marketCache.ts
4977
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
5228
+ import { readFileSync as readFileSync2 } from "fs";
5229
+ import { join as join2 } from "path";
5230
+
5231
+ // src/paths.ts
5232
+ import { closeSync, mkdirSync, openSync, renameSync, writeSync } from "fs";
4978
5233
  import { homedir } from "os";
4979
- import { join } from "path";
4980
- var POSITIVE_TTL_MS = 10 * 6e4;
4981
- var NEGATIVE_TTL_MS = 3 * 6e4;
5234
+ import { dirname, join } from "path";
4982
5235
  function cacheDir() {
4983
5236
  const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
4984
5237
  return join(base, "claudinho");
4985
5238
  }
5239
+ function writeFileAtomic(path, data) {
5240
+ mkdirSync(dirname(path), { recursive: true });
5241
+ const tmp = `${path}.${process.pid}.tmp`;
5242
+ const fd = openSync(tmp, "w");
5243
+ try {
5244
+ writeSync(fd, data);
5245
+ } finally {
5246
+ closeSync(fd);
5247
+ }
5248
+ renameSync(tmp, path);
5249
+ }
5250
+
5251
+ // src/marketCache.ts
5252
+ var POSITIVE_TTL_MS = 10 * 6e4;
5253
+ var NEGATIVE_TTL_MS = 3 * 6e4;
4986
5254
  function cachePath() {
4987
- return join(cacheDir(), "market-signals.json");
5255
+ return join2(cacheDir(), "market-signals.json");
4988
5256
  }
4989
5257
  function readFile() {
4990
5258
  try {
4991
- return JSON.parse(readFileSync(cachePath(), "utf8"));
5259
+ return JSON.parse(readFileSync2(cachePath(), "utf8"));
4992
5260
  } catch {
4993
5261
  return void 0;
4994
5262
  }
@@ -5019,23 +5287,18 @@ function writeMarketCache(source, competition, attempted, fetched, now = Date.no
5019
5287
  for (const id of attempted) {
5020
5288
  base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
5021
5289
  }
5022
- mkdirSync(cacheDir(), { recursive: true });
5023
- const tmp = join(cacheDir(), `market-signals.${process.pid}.tmp`);
5024
- writeFileSync(tmp, JSON.stringify(base));
5025
- renameSync(tmp, cachePath());
5290
+ writeFileAtomic(cachePath(), JSON.stringify(base));
5026
5291
  } catch {
5027
5292
  }
5028
5293
  }
5029
5294
 
5030
5295
  // src/starNudge.ts
5031
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
5032
- import { homedir as homedir2 } from "os";
5033
- import { dirname, join as join2 } from "path";
5296
+ import { readFileSync as readFileSync3 } from "fs";
5297
+ import { join as join3 } from "path";
5034
5298
  var REPO_URL = "https://github.com/arturogarrido/claudinho";
5035
5299
  var NUDGE_EVERY = 5;
5036
5300
  function counterPath() {
5037
- const base = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
5038
- return join2(base, "claudinho", "runs.json");
5301
+ return join3(cacheDir(), "runs.json");
5039
5302
  }
5040
5303
  function shouldNudge(runCount, every = NUDGE_EVERY) {
5041
5304
  return runCount > 0 && runCount % every === 0;
@@ -5044,14 +5307,13 @@ function bumpRunCount(path = counterPath()) {
5044
5307
  try {
5045
5308
  let count = 0;
5046
5309
  try {
5047
- const raw = JSON.parse(readFileSync2(path, "utf8"));
5310
+ const raw = JSON.parse(readFileSync3(path, "utf8"));
5048
5311
  if (typeof raw.count === "number" && Number.isFinite(raw.count)) count = raw.count;
5049
5312
  } catch {
5050
5313
  count = 0;
5051
5314
  }
5052
5315
  count += 1;
5053
- mkdirSync2(dirname(path), { recursive: true });
5054
- writeFileSync2(path, JSON.stringify({ count }), "utf8");
5316
+ writeFileAtomic(path, JSON.stringify({ count }));
5055
5317
  return count;
5056
5318
  } catch {
5057
5319
  return void 0;
@@ -5086,53 +5348,56 @@ function copyToClipboard(text, platform = process.platform) {
5086
5348
 
5087
5349
  // src/cache.ts
5088
5350
  import {
5089
- closeSync,
5090
- mkdirSync as mkdirSync3,
5091
- openSync,
5092
- readFileSync as readFileSync3,
5093
- renameSync as renameSync2,
5351
+ closeSync as closeSync2,
5352
+ mkdirSync as mkdirSync2,
5353
+ openSync as openSync2,
5354
+ readFileSync as readFileSync4,
5094
5355
  rmSync,
5095
5356
  statSync,
5096
- writeSync
5357
+ writeSync as writeSync2
5097
5358
  } from "fs";
5098
- import { homedir as homedir3 } from "os";
5099
- import { join as join3 } from "path";
5359
+ import { join as join4 } from "path";
5360
+ var CACHE_VERSION = 2;
5100
5361
  var LOCK_STALE_MS = 6e4;
5101
- function cacheDir2() {
5102
- const base = process.env.XDG_CACHE_HOME || join3(homedir3(), ".cache");
5103
- return join3(base, "claudinho");
5104
- }
5105
- function cachePath2() {
5106
- return join3(cacheDir2(), "state.json");
5362
+ function cachePath2(source = "espn", competition = DEFAULT_COMPETITION) {
5363
+ if (source === "espn" && competition === DEFAULT_COMPETITION) {
5364
+ return join4(cacheDir(), "state.json");
5365
+ }
5366
+ const slug = `${source}.${competition}`.replace(/[^a-zA-Z0-9._-]/g, "_");
5367
+ return join4(cacheDir(), `state.${slug}.json`);
5107
5368
  }
5108
5369
  function lockPath() {
5109
- return join3(cacheDir2(), "refresh.lock");
5370
+ return join4(cacheDir(), "refresh.lock");
5110
5371
  }
5111
- function readState() {
5372
+ function readState(source = "espn", competition = DEFAULT_COMPETITION) {
5112
5373
  try {
5113
- return JSON.parse(readFileSync3(cachePath2(), "utf8"));
5374
+ const s = JSON.parse(
5375
+ readFileSync4(cachePath2(source, competition), "utf8")
5376
+ );
5377
+ return s.version === CACHE_VERSION ? s : void 0;
5114
5378
  } catch {
5115
5379
  return void 0;
5116
5380
  }
5117
5381
  }
5118
5382
  function readCurrentState(source, competition) {
5119
- const s = readState();
5383
+ const s = readState(source, competition);
5120
5384
  return s && s.source === source && s.competition === competition ? s : void 0;
5121
5385
  }
5122
5386
  function writeState(state) {
5123
- const dir = cacheDir2();
5124
- mkdirSync3(dir, { recursive: true });
5125
- const tmp = join3(dir, `state.${process.pid}.tmp`);
5126
- writeFileSync_(tmp, JSON.stringify(state));
5127
- renameSync2(tmp, cachePath2());
5128
- }
5129
- function writeFileSync_(path, data) {
5130
- const fd = openSync(path, "w");
5131
- try {
5132
- writeSync(fd, data);
5133
- } finally {
5134
- closeSync(fd);
5135
- }
5387
+ writeFileAtomic(
5388
+ cachePath2(state.source, state.competition),
5389
+ JSON.stringify({ ...state, version: CACHE_VERSION })
5390
+ );
5391
+ }
5392
+ function backoffActive(state, now = Date.now()) {
5393
+ if (!state?.backoffUntil) return false;
5394
+ const t2 = Date.parse(state.backoffUntil);
5395
+ return Number.isFinite(t2) && now < t2;
5396
+ }
5397
+ function fixturesAttemptAgeMs(state, now = Date.now()) {
5398
+ if (!state?.fixturesAttemptedAt) return Infinity;
5399
+ const t2 = Date.parse(state.fixturesAttemptedAt);
5400
+ return Number.isFinite(t2) ? now - t2 : Infinity;
5136
5401
  }
5137
5402
  function ageMs(state, now = Date.now()) {
5138
5403
  if (!state) return Infinity;
@@ -5147,7 +5412,7 @@ function fixturesAgeMs(state, now = Date.now()) {
5147
5412
  function lockAgeMs(now = Date.now()) {
5148
5413
  const lp = lockPath();
5149
5414
  try {
5150
- const contents = readFileSync3(lp, "utf8");
5415
+ const contents = readFileSync4(lp, "utf8");
5151
5416
  const written = Number.parseInt(contents.split(/\s+/)[1] ?? "", 10);
5152
5417
  if (Number.isFinite(written)) return now - written;
5153
5418
  } catch {
@@ -5163,14 +5428,14 @@ function isLockFresh(now = Date.now()) {
5163
5428
  return lockAgeMs(now) < LOCK_STALE_MS;
5164
5429
  }
5165
5430
  function acquireLock(now = Date.now()) {
5166
- mkdirSync3(cacheDir2(), { recursive: true });
5431
+ mkdirSync2(cacheDir(), { recursive: true });
5167
5432
  const lp = lockPath();
5168
5433
  try {
5169
- const fd = openSync(lp, "wx");
5434
+ const fd = openSync2(lp, "wx");
5170
5435
  try {
5171
- writeSync(fd, `${process.pid} ${now}`);
5436
+ writeSync2(fd, `${process.pid} ${now}`);
5172
5437
  } finally {
5173
- closeSync(fd);
5438
+ closeSync2(fd);
5174
5439
  }
5175
5440
  return true;
5176
5441
  } catch {
@@ -5181,11 +5446,11 @@ function acquireLock(now = Date.now()) {
5181
5446
  return false;
5182
5447
  }
5183
5448
  try {
5184
- const fd = openSync(lp, "wx");
5449
+ const fd = openSync2(lp, "wx");
5185
5450
  try {
5186
- writeSync(fd, `${process.pid} ${now}`);
5451
+ writeSync2(fd, `${process.pid} ${now}`);
5187
5452
  } finally {
5188
- closeSync(fd);
5453
+ closeSync2(fd);
5189
5454
  }
5190
5455
  return true;
5191
5456
  } catch {
@@ -5218,6 +5483,10 @@ function inLiveWindow(now = Date.now(), fixtures = allFixtures()) {
5218
5483
  function isResolvedFixture(m) {
5219
5484
  return isResolvedNation(m.home) && isResolvedNation(m.away);
5220
5485
  }
5486
+ function isMatchShaped(m) {
5487
+ const x = m;
5488
+ return !!x && typeof x === "object" && typeof x.id === "string" && typeof x.kickoff === "string" && !!x.home?.code && !!x.away?.code;
5489
+ }
5221
5490
  function nextOverall(now, fixtures = allFixtures()) {
5222
5491
  return [...fixtures].sort(byKickoff).find((m) => Date.parse(m.kickoff) >= now && isResolvedFixture(m));
5223
5492
  }
@@ -5238,7 +5507,7 @@ function liveMatchesFromCache(state, nowMs = Date.now()) {
5238
5507
  const liveArr = fresh && Array.isArray(state?.live) ? state.live : [];
5239
5508
  return liveArr.filter(
5240
5509
  (m) => !!m && typeof m === "object" && isLive(m.status) && !!m.home?.code && !!m.away?.code
5241
- );
5510
+ ).map(sanitizeMatchStrings);
5242
5511
  }
5243
5512
  function renderPrompt(state, opts = {}) {
5244
5513
  const now = opts.now ?? /* @__PURE__ */ new Date();
@@ -5247,7 +5516,7 @@ function renderPrompt(state, opts = {}) {
5247
5516
  const flags = opts.flags ?? true;
5248
5517
  const team = opts.team?.toUpperCase();
5249
5518
  const live = liveMatchesFromCache(state, nowMs);
5250
- const cachedFixtures = Array.isArray(state?.fixtures) ? state.fixtures : [];
5519
+ const cachedFixtures = Array.isArray(state?.fixtures) ? state.fixtures.filter(isMatchShaped).map(sanitizeMatchStrings) : [];
5251
5520
  const schedule = cachedFixtures.length ? mergeLive(allFixtures(), cachedFixtures) : void 0;
5252
5521
  if (team) {
5253
5522
  const mine = live.find((m) => m.home?.code === team || m.away?.code === team);
@@ -5280,10 +5549,16 @@ function renderPrompt(state, opts = {}) {
5280
5549
  }
5281
5550
 
5282
5551
  // src/hook.ts
5552
+ function rosterPinned(t2) {
5553
+ const { team } = lookupTeam(t2.code);
5554
+ return team ? { ...t2, name: team.name, flag: team.flag } : t2;
5555
+ }
5283
5556
  function line(m, flags) {
5284
5557
  const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
5285
- const home = flags ? `${m.home.flag} ${m.home.name}` : m.home.name;
5286
- const away = flags ? `${m.away.name} ${m.away.flag}` : m.away.name;
5558
+ const h = rosterPinned(m.home);
5559
+ const a = rosterPinned(m.away);
5560
+ const home = flags ? `${h.flag} ${h.name}` : h.name;
5561
+ const away = flags ? `${a.name} ${a.flag}` : a.name;
5287
5562
  return `${home} ${scoreline(m)} ${away} (${minute})`;
5288
5563
  }
5289
5564
  function renderHook(state, opts = {}) {
@@ -5309,9 +5584,12 @@ import { spawn } from "child_process";
5309
5584
  var MIN_REFRESH_MS = 12e3;
5310
5585
  var FIXTURES_TTL_MS = 15 * 6e4;
5311
5586
  var FIXTURES_EMPTY_TTL_MS = 6e4;
5587
+ var BACKOFF_MS = 5 * 6e4;
5588
+ var BACKOFF_JITTER_MS = 6e4;
5312
5589
  function fixturesStale(state, now) {
5313
5590
  const ttl = (state?.fixtures?.length ?? 0) > 0 ? FIXTURES_TTL_MS : FIXTURES_EMPTY_TTL_MS;
5314
- return fixturesAgeMs(state, now) > ttl;
5591
+ if (fixturesAgeMs(state, now) <= ttl) return false;
5592
+ return fixturesAttemptAgeMs(state, now) > FIXTURES_EMPTY_TTL_MS;
5315
5593
  }
5316
5594
  function nextStaticUpcoming(nowMs) {
5317
5595
  return [...allFixtures()].sort(byKickoff).find((m) => Date.parse(m.kickoff) >= nowMs);
@@ -5325,24 +5603,57 @@ function liveWindowActive(nowMs) {
5325
5603
  if (resolveCompetition() !== DEFAULT_COMPETITION) return true;
5326
5604
  return inLiveWindow(nowMs);
5327
5605
  }
5328
- function liveAdapter() {
5606
+ function liveAdapter(source) {
5329
5607
  const competition = resolveCompetition();
5330
- if (competition === DEFAULT_COMPETITION) {
5608
+ if (source === "espn" && competition === DEFAULT_COMPETITION) {
5331
5609
  return new EspnAdapter({ enrichGroups: false });
5332
5610
  }
5333
- return makeAdapter("espn");
5611
+ return makeAdapter(source);
5334
5612
  }
5335
5613
  async function runRefresh(opts = {}) {
5336
5614
  const now = opts.now ?? /* @__PURE__ */ new Date();
5337
5615
  const nowMs = now.getTime();
5338
5616
  const source = opts.source ?? "espn";
5339
5617
  const competition = resolveCompetition();
5340
- const cached = readState();
5618
+ if (!KNOWN_SOURCES.includes(source)) {
5619
+ if (!readState(source, competition) && acquireLock()) {
5620
+ try {
5621
+ writeState({
5622
+ updatedAt: now.toISOString(),
5623
+ live: [],
5624
+ degraded: true,
5625
+ // no live provider served this scope โ€” never claim otherwise
5626
+ source,
5627
+ competition
5628
+ });
5629
+ } finally {
5630
+ releaseLock();
5631
+ }
5632
+ }
5633
+ return;
5634
+ }
5635
+ const cached = readState(source, competition);
5341
5636
  const sameScope = !!cached && cached.source === source && cached.competition === competition;
5342
5637
  const base = sameScope ? cached : void 0;
5343
- const needLive = liveWindowActive(nowMs) && (!base || ageMs(base, nowMs) >= MIN_REFRESH_MS);
5344
- const needFixtures = inKnockoutPhase(nowMs) && fixturesStale(base, nowMs);
5345
- if (!needLive && !needFixtures) return;
5638
+ const inBackoff = backoffActive(base, nowMs);
5639
+ const needLive = !inBackoff && liveWindowActive(nowMs) && (!base || ageMs(base, nowMs) >= MIN_REFRESH_MS);
5640
+ const needFixtures = !inBackoff && inKnockoutPhase(nowMs) && fixturesStale(base, nowMs);
5641
+ if (!needLive && !needFixtures) {
5642
+ if (!base && acquireLock()) {
5643
+ try {
5644
+ writeState({
5645
+ updatedAt: now.toISOString(),
5646
+ live: [],
5647
+ degraded: false,
5648
+ source,
5649
+ competition
5650
+ });
5651
+ } finally {
5652
+ releaseLock();
5653
+ }
5654
+ }
5655
+ return;
5656
+ }
5346
5657
  if (!acquireLock()) return;
5347
5658
  try {
5348
5659
  let live = base?.live ?? [];
@@ -5350,9 +5661,12 @@ async function runRefresh(opts = {}) {
5350
5661
  let updatedAt = base?.updatedAt ?? now.toISOString();
5351
5662
  let fixtures = base?.fixtures;
5352
5663
  let fixturesUpdatedAt = base?.fixturesUpdatedAt;
5664
+ let fixturesAttemptedAt = base?.fixturesAttemptedAt;
5665
+ let backoffUntil = base?.backoffUntil;
5666
+ const adapter = liveAdapter(source);
5353
5667
  if (needLive) {
5354
5668
  try {
5355
- const r = await getLiveMatches(liveAdapter(), now);
5669
+ const r = await getLiveMatches(adapter, now);
5356
5670
  live = r.matches;
5357
5671
  degraded = r.degraded;
5358
5672
  } catch {
@@ -5361,8 +5675,9 @@ async function runRefresh(opts = {}) {
5361
5675
  updatedAt = now.toISOString();
5362
5676
  }
5363
5677
  if (needFixtures) {
5678
+ fixturesAttemptedAt = now.toISOString();
5364
5679
  try {
5365
- const r = await getKnockoutFixtures(liveAdapter(), now);
5680
+ const r = await getKnockoutFixtures(adapter, now);
5366
5681
  if (!r.degraded) {
5367
5682
  fixtures = r.fixtures;
5368
5683
  fixturesUpdatedAt = now.toISOString();
@@ -5370,6 +5685,13 @@ async function runRefresh(opts = {}) {
5370
5685
  } catch {
5371
5686
  }
5372
5687
  }
5688
+ if (adapter.lastError?.throttled) {
5689
+ backoffUntil = new Date(
5690
+ nowMs + BACKOFF_MS + Math.floor(Math.random() * BACKOFF_JITTER_MS)
5691
+ ).toISOString();
5692
+ } else if (backoffUntil && Date.parse(backoffUntil) <= nowMs) {
5693
+ backoffUntil = void 0;
5694
+ }
5373
5695
  writeState({
5374
5696
  updatedAt,
5375
5697
  live,
@@ -5377,19 +5699,23 @@ async function runRefresh(opts = {}) {
5377
5699
  source,
5378
5700
  competition,
5379
5701
  ...fixtures ? { fixtures } : {},
5380
- ...fixturesUpdatedAt ? { fixturesUpdatedAt } : {}
5702
+ ...fixturesUpdatedAt ? { fixturesUpdatedAt } : {},
5703
+ ...fixturesAttemptedAt ? { fixturesAttemptedAt } : {},
5704
+ ...backoffUntil ? { backoffUntil } : {}
5381
5705
  });
5382
5706
  } finally {
5383
5707
  releaseLock();
5384
5708
  }
5385
5709
  }
5386
- function shouldRefresh(now = Date.now()) {
5710
+ function shouldRefresh(now = Date.now(), state = readState("espn", resolveCompetition())) {
5387
5711
  if (!liveWindowActive(now)) return false;
5712
+ if (backoffActive(state, now)) return false;
5388
5713
  if (isLockFresh(now)) return false;
5389
- return ageMs(readState(), now) > LIVE_TTL_MS;
5714
+ return ageMs(state, now) > LIVE_TTL_MS;
5390
5715
  }
5391
- function shouldRefreshFixtures(now = Date.now(), state = readState()) {
5716
+ function shouldRefreshFixtures(now = Date.now(), state = readState("espn", resolveCompetition())) {
5392
5717
  if (!inKnockoutPhase(now)) return false;
5718
+ if (backoffActive(state, now)) return false;
5393
5719
  if (isLockFresh(now)) return false;
5394
5720
  return fixturesStale(state, now);
5395
5721
  }
@@ -5399,81 +5725,25 @@ function spawnRefresh(source) {
5399
5725
  if (!entry) return;
5400
5726
  const child = spawn(process.execPath, [entry, "_refresh", "--source", source], {
5401
5727
  detached: true,
5402
- stdio: "ignore"
5728
+ stdio: "ignore",
5729
+ // Windows: a detached child gets its own console window without this โ€”
5730
+ // the statusline would flash one every ~12โ€“15s during live matches.
5731
+ windowsHide: true
5403
5732
  });
5404
5733
  child.unref();
5405
5734
  } catch {
5406
5735
  }
5407
5736
  }
5408
5737
 
5409
- // src/cursorPayload.ts
5410
- import { readFileSync as readFileSync4 } from "fs";
5411
- function parseCursorPayload(raw) {
5412
- try {
5413
- const trimmed = raw.trim();
5414
- if (!trimmed) return void 0;
5415
- return JSON.parse(trimmed);
5416
- } catch {
5417
- return void 0;
5418
- }
5419
- }
5420
- function readCursorPayload() {
5421
- try {
5422
- if (process.stdin.isTTY) return void 0;
5423
- return parseCursorPayload(readFileSync4(0, "utf8"));
5424
- } catch {
5425
- return void 0;
5426
- }
5427
- }
5428
- function looksLikeCursorPayload(payload) {
5429
- return !!(payload.model?.display_name || payload.context_window != null || payload.render_width_chars != null || payload.worktree?.name || payload.vim?.mode);
5430
- }
5431
- function cursorMetaEnabled(payload) {
5432
- const v = (process.env.CLAUDINHO_CURSOR_META ?? "").toLowerCase();
5433
- if (v === "0" || v === "false" || v === "no") return false;
5434
- if (v === "1" || v === "true" || v === "yes") return !!payload;
5435
- if (v === "auto") return !!payload && looksLikeCursorPayload(payload);
5436
- return false;
5437
- }
5438
- function renderCursorMetaLine(payload) {
5439
- const parts = [];
5440
- const model = payload.model?.display_name;
5441
- if (model) {
5442
- let label = model;
5443
- if (payload.model?.param_summary) label += ` ${payload.model.param_summary}`;
5444
- parts.push(label);
5445
- }
5446
- const pct2 = payload.context_window?.used_percentage;
5447
- if (typeof pct2 === "number" && Number.isFinite(pct2)) {
5448
- parts.push(`ctx ${Math.floor(pct2)}%`);
5449
- }
5450
- if (payload.worktree?.name) parts.push(`wt ${payload.worktree.name}`);
5451
- if (payload.vim?.mode) parts.push(payload.vim.mode);
5452
- if (parts.length === 0) return void 0;
5453
- return `\x1B[90m${parts.join(" ")}\x1B[0m`;
5454
- }
5455
- function renderPromptOutput(scoreLine, payload) {
5456
- const meta = payload && cursorMetaEnabled(payload) ? renderCursorMetaLine(payload) : void 0;
5457
- if (meta) return `${scoreLine}
5458
- ${meta}`;
5459
- return scoreLine;
5460
- }
5461
-
5462
5738
  // src/install.ts
5463
- import {
5464
- copyFileSync,
5465
- existsSync,
5466
- mkdirSync as mkdirSync4,
5467
- readFileSync as readFileSync5,
5468
- writeFileSync as writeFileSync3
5469
- } from "fs";
5470
- import { homedir as homedir4 } from "os";
5471
- import { dirname as dirname2, join as join4 } from "path";
5739
+ import { copyFileSync, existsSync, readFileSync as readFileSync5 } from "fs";
5740
+ import { homedir as homedir2 } from "os";
5741
+ import { join as join5 } from "path";
5472
5742
  function claudeSettingsPath() {
5473
- return join4(homedir4(), ".claude", "settings.json");
5743
+ return join5(homedir2(), ".claude", "settings.json");
5474
5744
  }
5475
5745
  function cursorCliConfigPath() {
5476
- return join4(homedir4(), ".cursor", "cli-config.json");
5746
+ return join5(homedir2(), ".cursor", "cli-config.json");
5477
5747
  }
5478
5748
  function isSameCommand(configured, requested) {
5479
5749
  return configured === requested;
@@ -5535,8 +5805,7 @@ function initStatuslineFor(target, opts = {}) {
5535
5805
  }
5536
5806
  backupOnce(path);
5537
5807
  settings.statusLine = sl;
5538
- mkdirSync4(dirname2(path), { recursive: true });
5539
- writeFileSync3(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
5808
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n");
5540
5809
  const surface = target === "cursor" ? "Cursor CLI statusline" : "Statusline";
5541
5810
  return {
5542
5811
  action: "written",
@@ -5582,8 +5851,7 @@ function initHook(opts = {}) {
5582
5851
  const hooks = settings.hooks;
5583
5852
  hooks[CLAUDE_HOOK_EVENT] ??= [];
5584
5853
  hooks[CLAUDE_HOOK_EVENT].push({ hooks: [{ type: "command", command }] });
5585
- mkdirSync4(dirname2(path), { recursive: true });
5586
- writeFileSync3(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
5854
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n");
5587
5855
  return {
5588
5856
  action: "written",
5589
5857
  path,
@@ -5654,6 +5922,11 @@ function precheck(cfg, t2, date) {
5654
5922
  if (cfg.tz && !isValidTimeZone(cfg.tz)) {
5655
5923
  process.stderr.write(t2("warn.tz", { tz: cfg.tz }) + "\n");
5656
5924
  }
5925
+ if (!KNOWN_SOURCES.includes(cfg.source)) {
5926
+ throw new InputError(
5927
+ t2("err.source", { source: cfg.source, sources: KNOWN_SOURCES.join(", ") })
5928
+ );
5929
+ }
5657
5930
  const competition = resolveCompetition();
5658
5931
  if (competition !== DEFAULT_COMPETITION) {
5659
5932
  process.stderr.write(
@@ -5665,18 +5938,24 @@ function precheck(cfg, t2, date) {
5665
5938
  throw new InputError(t2("err.date", { date }));
5666
5939
  }
5667
5940
  }
5668
- function resolveTeamArg(team, usage) {
5941
+ function resolveTeamArg(team, usage, t2) {
5669
5942
  const raw = team ?? process.env.CLAUDINHO_TEAM;
5670
5943
  if (!raw) throw new InputError(usage);
5671
5944
  const { team: hit, matches } = lookupTeam(raw);
5672
5945
  if (hit) return hit.code;
5673
5946
  if (matches.length > 1) {
5674
5947
  throw new InputError(
5675
- `"${raw}" is ambiguous \u2014 did you mean ${matches.map((m) => `${m.name} (${m.code})`).join(", ")}? Use the 3-letter code.`
5948
+ `${t2("team.ambiguous", { query: raw })} ${matches.map((m) => `${m.name} (${m.code})`).join(", ")}`
5676
5949
  );
5677
5950
  }
5678
5951
  if (/^[A-Za-z]{3}$/.test(raw)) return raw.toUpperCase();
5679
- throw new InputError(`No team found for "${raw}". Use a nation name or 3-letter code (e.g. Mexico, MEX).`);
5952
+ throw new InputError(t2("team.none", { query: raw }));
5953
+ }
5954
+ function resolveEnvTeam(raw) {
5955
+ if (!raw) return void 0;
5956
+ const { team } = lookupTeam(raw);
5957
+ if (team) return team.code;
5958
+ return /^[A-Za-z]{3}$/.test(raw) ? raw.toUpperCase() : void 0;
5680
5959
  }
5681
5960
  async function cmdToday(date, ctx) {
5682
5961
  const { cfg, t: t2 } = ctx;
@@ -5748,7 +6027,7 @@ async function cmdLive(ctx) {
5748
6027
  async function cmdNext(team, ctx) {
5749
6028
  const { cfg, t: t2, now } = ctx;
5750
6029
  precheck(cfg, t2);
5751
- const code = resolveTeamArg(team, "Usage: claudinho next <team> (or set CLAUDINHO_TEAM)");
6030
+ const code = resolveTeamArg(team, "Usage: claudinho next <team> (or set CLAUDINHO_TEAM)", t2);
5752
6031
  const { fixture, degraded, source } = await getNextFixtureForTeam(
5753
6032
  adapterFor(ctx),
5754
6033
  code,
@@ -5770,7 +6049,7 @@ async function cmdNext(team, ctx) {
5770
6049
  out(header(t2("next.label", { team: code }), c));
5771
6050
  out();
5772
6051
  out(matchLine(fixture, cfg, t2, c, flags));
5773
- const stage = fixture.stage !== "GROUP" ? `${stageLabel(fixture)} \xB7 ` : "";
6052
+ const stage = fixture.stage !== "GROUP" ? `${stageLabelI18n(cfg.lang, fixture.stage)} \xB7 ` : "";
5774
6053
  out(
5775
6054
  " " + c.dim(
5776
6055
  `${stage}${formatKickoff(fixture.kickoff, { tz: cfg.tz, locale: cfg.lang })} \xB7 ` + t2("next.in", { countdown: countdown(fixture.kickoff) })
@@ -5928,10 +6207,10 @@ async function cmdBracket(stage, opts, ctx) {
5928
6207
  out(disclaimer(t2, c));
5929
6208
  maybeStarNudge(ctx);
5930
6209
  }
5931
- function cmdPrompt({ cfg }) {
6210
+ function cmdPrompt({ cfg }, io = {}) {
5932
6211
  try {
5933
- const payload = readCursorPayload();
5934
- const team = process.env.CLAUDINHO_TEAM;
6212
+ const payload = "cursor" in io ? io.cursor : readCursorPayload();
6213
+ const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
5935
6214
  const compact = !["0", "false", "no"].includes(
5936
6215
  (process.env.CLAUDINHO_COMPACT ?? "").toLowerCase()
5937
6216
  );
@@ -5940,7 +6219,7 @@ function cmdPrompt({ cfg }) {
5940
6219
  const state = readCurrentState(cfg.source, resolveCompetition());
5941
6220
  const scoreLine = renderPrompt(state, { team, compact, max, flags: flagsEnabled() });
5942
6221
  out(renderPromptOutput(scoreLine, payload));
5943
- if (!state || shouldRefresh() || shouldRefreshFixtures(Date.now(), state)) {
6222
+ if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
5944
6223
  spawnRefresh(cfg.source);
5945
6224
  }
5946
6225
  } catch {
@@ -5949,11 +6228,11 @@ function cmdPrompt({ cfg }) {
5949
6228
  }
5950
6229
  function cmdHook({ cfg }) {
5951
6230
  try {
5952
- const team = process.env.CLAUDINHO_TEAM;
6231
+ const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
5953
6232
  const state = readCurrentState(cfg.source, resolveCompetition());
5954
6233
  const ctx = renderHook(state, { team, flags: flagsEnabled() });
5955
6234
  if (ctx) out(ctx);
5956
- if (!state || shouldRefresh() || shouldRefreshFixtures(Date.now(), state)) {
6235
+ if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
5957
6236
  spawnRefresh(cfg.source);
5958
6237
  }
5959
6238
  } catch {
@@ -6001,7 +6280,8 @@ function cmdInitCursor(opts, { cfg }) {
6001
6280
  out(CURSOR_MCP_SNIPPET);
6002
6281
  return;
6003
6282
  }
6004
- printInitResult(initCursorStatusline(), cfg);
6283
+ const res = initCursorStatusline();
6284
+ printInitResult(res, cfg);
6005
6285
  out("");
6006
6286
  out("Optional \u2014 live MCP tools in Cursor: add to ~/.cursor/mcp.json (or project .cursor/mcp.json):");
6007
6287
  out(CURSOR_MCP_SNIPPET);
@@ -6009,7 +6289,7 @@ function cmdInitCursor(opts, { cfg }) {
6009
6289
  out("Tip: export CLAUDINHO_CURSOR_META=auto for a model + context line below the score.");
6010
6290
  out("");
6011
6291
  out("\u2192 Restart your agent session to see it.");
6012
- printInitStarCta(cfg);
6292
+ if (res.action === "written") printInitStarCta(cfg);
6013
6293
  }
6014
6294
  function cmdInitClaude(opts, { cfg }) {
6015
6295
  if (opts.print) {
@@ -6023,14 +6303,16 @@ function cmdInitClaude(opts, { cfg }) {
6023
6303
  out(CLAUDE_MCP_ONELINER);
6024
6304
  return;
6025
6305
  }
6026
- printInitResult(initStatusline(), cfg);
6027
- printInitResult(initHook(), cfg);
6306
+ const statusRes = initStatusline();
6307
+ const hookRes = initHook();
6308
+ printInitResult(statusRes, cfg);
6309
+ printInitResult(hookRes, cfg);
6028
6310
  out("");
6029
6311
  out("Next \u2014 add the MCP server:");
6030
6312
  out(` ${CLAUDE_MCP_ONELINER}`);
6031
6313
  out("");
6032
6314
  out("\u2192 Restart Claude Code to see it.");
6033
- printInitStarCta(cfg);
6315
+ if (statusRes.action === "written" || hookRes.action === "written") printInitStarCta(cfg);
6034
6316
  }
6035
6317
  async function cmdMatch(id, ctx) {
6036
6318
  const { cfg, t: t2 } = ctx;
@@ -6054,7 +6336,7 @@ async function cmdMatch(id, ctx) {
6054
6336
  out(disclaimer(t2, c));
6055
6337
  return;
6056
6338
  }
6057
- const stageLabelText = stageLabel(match);
6339
+ const stageLabelText = stageLabelI18n(cfg.lang, match.stage, match.group ?? void 0);
6058
6340
  out(header(`${match.home.name} ${scoreline(match)} ${match.away.name}`, c));
6059
6341
  out(" " + c.dim(`${stageLabelText} \xB7 ${matchLocation(match)}`));
6060
6342
  out(
@@ -6100,7 +6382,7 @@ async function cmdMarkets(target, team, ctx) {
6100
6382
  const { cfg, t: t2 } = ctx;
6101
6383
  if (target === "next") {
6102
6384
  precheck(cfg, t2);
6103
- const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)");
6385
+ const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)", t2);
6104
6386
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
6105
6387
  const { match: fixture, degraded } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
6106
6388
  const sig = fixture && marketRelevant(fixture, now2) ? (await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS)).get(fixture.id) : void 0;
@@ -6376,7 +6658,7 @@ async function cmdShare(target, team, opts, ctx) {
6376
6658
  }
6377
6659
  if (target === "next") {
6378
6660
  precheck(cfg, t2);
6379
- const code = resolveTeamArg(team, "Usage: claudinho share next <team> (or set CLAUDINHO_TEAM)");
6661
+ const code = resolveTeamArg(team, "Usage: claudinho share next <team> (or set CLAUDINHO_TEAM)", t2);
6380
6662
  const { fixture, degraded: degraded2, source: source2 } = await getNextFixtureForTeam(
6381
6663
  adapterFor(ctx),
6382
6664
  code,
@@ -6528,6 +6810,7 @@ function maybeStarNudge(ctx) {
6528
6810
  out(c.dim(` \u2B50 Enjoying Claudinho? Star it \u2192 ${REPO_URL} (claudinho star)`));
6529
6811
  }
6530
6812
  function printInitStarCta(cfg) {
6813
+ if (cfg.json || !process.stdout.isTTY || process.env.CLAUDINHO_NO_STAR) return;
6531
6814
  const c = painterFor(cfg);
6532
6815
  out("");
6533
6816
  out(c.dim(`\u2B50 If this keeps you in the flow during the match, star the repo \u2192 ${REPO_URL}`));
@@ -6541,7 +6824,8 @@ function cmdVibe(ctx) {
6541
6824
  const state = readCurrentState(cfg.source, resolveCompetition());
6542
6825
  liveSeg = vibeLiveSegment(
6543
6826
  liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()),
6544
- process.env.CLAUDINHO_TEAM
6827
+ // Name-or-code, matching the statusline/hook (offline lookup).
6828
+ resolveEnvTeam(process.env.CLAUDINHO_TEAM)
6545
6829
  );
6546
6830
  } catch {
6547
6831
  }
@@ -6565,7 +6849,7 @@ function handlePipeError(stream) {
6565
6849
  }
6566
6850
  handlePipeError(process.stdout);
6567
6851
  handlePipeError(process.stderr);
6568
- var VERSION = "0.8.18";
6852
+ var VERSION = "0.9.0";
6569
6853
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
6570
6854
  function ctxFrom(cmd) {
6571
6855
  let root = cmd;
@@ -6651,8 +6935,9 @@ program.command("share").description("print a shareable, copy-pasteable match sn
6651
6935
  fail(e);
6652
6936
  }
6653
6937
  });
6654
- program.command("prompt").description("print a status line (Claude Code / Cursor CLI statusline, tmux, Starship, \u2026)").action((_opts, cmd) => {
6655
- cmdPrompt(ctxFrom(cmd));
6938
+ program.command("prompt").description("print a status line (Claude Code / Cursor CLI statusline, tmux, Starship, \u2026)").action(async (_opts, cmd) => {
6939
+ const cursor = await readCursorPayloadBounded();
6940
+ cmdPrompt(ctxFrom(cmd), { cursor });
6656
6941
  });
6657
6942
  var init = program.command("init").description("one-step setup for your agent: `init cursor` or `init claude`");
6658
6943
  init.command("cursor").description("set up claudinho for the Cursor CLI (statusline + MCP config)").option("--print", "print the config snippets for manual install instead of writing them").action((opts, cmd) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.8.18",
3
+ "version": "0.9.0",
4
4
  "description": "Live scores, fixtures, group tables, and read-only prediction-market signals for the 2026 men's football tournament โ€” in your terminal, Claude Code, and Cursor CLI statusline. No API keys. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,10 +58,11 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.20.0",
61
+ "@vitest/coverage-v8": "^4.1.9",
61
62
  "tsup": "^8.0.0",
62
63
  "typescript": "^5.7.0",
63
64
  "vitest": "^4.1.9",
64
- "@claudinho/core": "0.8.18"
65
+ "@claudinho/core": "0.9.0"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "tsup",