@claudinho/cli 0.8.17 → 0.8.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +252 -38
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -35,10 +35,11 @@ All 104 fixtures ship bundled, so the schedule works offline; only live scores h
35
35
  ```bash
36
36
  claudinho today [date] # a day's fixtures in your timezone (default: today), live scores inline
37
37
  claudinho live # matches in play right now
38
- claudinho next [TEAM] # a team's next fixture + countdown (default: $CLAUDINHO_TEAM)
38
+ claudinho next [TEAM] # a team's next fixture + countdown — TEAM is a name OR code (Mexico | MEX | "DR Congo"); default $CLAUDINHO_TEAM
39
39
  claudinho table [GROUP] # live cumulative group standings (default: all groups)
40
40
  claudinho bracket [STAGE] # knockout bracket (R32, R16, QF, SF, 3P, F); --tree for ASCII tree
41
41
  claudinho match <id> # a single match's detail
42
+ claudinho team <query> # resolve a name/code to its FIFA code, flag, and group (e.g. team "DR Congo")
42
43
  claudinho markets [target] # prediction-market signals: today | <date> | <id> | next <TEAM>
43
44
  # (next prefers the team's IN-PLAY match while one is live)
44
45
  claudinho share [target] # copy-pasteable snippet: today | live | <date> | <id> | next <TEAM> | table <GROUP> | bracket [STAGE]
@@ -185,7 +186,7 @@ The statusline reads from a local micro-cache and **never blocks on the
185
186
  network** (<150ms). When several matches are live it shows them all inline:
186
187
  `⚽ 🇳🇴 1–1 🇫🇷 87' · 🇸🇳 1–2 🇮🇶 86'`. Customize via env:
187
188
 
188
- - `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
189
190
  - `CLAUDINHO_MAX=2` — cap how many live matches show inline (rest collapse to `+N`; default: all)
190
191
  - `CLAUDINHO_COMPACT=0` — show 3-letter codes alongside flags
191
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
@@ -500,6 +500,63 @@ function shiftUtcDate(dateISO, days) {
500
500
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
501
501
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
502
502
  }
503
+ var FEED_TEXT_MAX = 100;
504
+ function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
505
+ let out2 = "";
506
+ let count = 0;
507
+ for (const ch of String(value)) {
508
+ const cp = ch.codePointAt(0) ?? 0;
509
+ const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
510
+ if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
511
+ if (count >= max) break;
512
+ out2 += isWhitespaceControl ? " " : ch;
513
+ count++;
514
+ }
515
+ return out2;
516
+ }
517
+ function sanitizeTeam(t2) {
518
+ return {
519
+ ...t2 ?? {},
520
+ code: sanitizeFeedText(t2?.code ?? ""),
521
+ name: sanitizeFeedText(t2?.name ?? ""),
522
+ flag: sanitizeFeedText(t2?.flag ?? "")
523
+ };
524
+ }
525
+ function finiteOrUndefined(v) {
526
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
527
+ }
528
+ function sanitizeScorePair(v) {
529
+ const home = finiteOrUndefined(v?.home);
530
+ const away = finiteOrUndefined(v?.away);
531
+ return home !== void 0 && away !== void 0 ? { home, away } : void 0;
532
+ }
533
+ function sanitizeMatchStrings(m) {
534
+ const score = sanitizeScorePair(m.score);
535
+ return {
536
+ ...m,
537
+ venue: sanitizeFeedText(m.venue ?? ""),
538
+ city: m.city == null ? m.city : sanitizeFeedText(m.city),
539
+ country: m.country == null ? m.country : sanitizeFeedText(m.country),
540
+ home: sanitizeTeam(m.home),
541
+ away: sanitizeTeam(m.away),
542
+ score,
543
+ shootout: score ? sanitizeScorePair(m.shootout) : void 0,
544
+ minute: finiteOrUndefined(m.minute)
545
+ };
546
+ }
547
+ var segmenter = new Intl.Segmenter();
548
+ var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
549
+ function displayWidth(s) {
550
+ let w = 0;
551
+ for (const { segment } of segmenter.segment(s)) {
552
+ w += WIDE_CLUSTER.test(segment) ? 2 : 1;
553
+ }
554
+ return w;
555
+ }
556
+ function padVisible(s, width) {
557
+ const w = displayWidth(s);
558
+ return w >= width ? s : s + " ".repeat(width - w);
559
+ }
503
560
  function outcomeFromScore(home, away) {
504
561
  if (home > away) return "H";
505
562
  if (home < away) return "A";
@@ -2808,6 +2865,60 @@ function groups(fixtures = SCHEDULE) {
2808
2865
  for (const m of fixtures) if (m.group) set.add(m.group);
2809
2866
  return [...set].sort();
2810
2867
  }
2868
+ function norm2(s) {
2869
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z]/g, "");
2870
+ }
2871
+ function allTeams(fixtures = allFixtures()) {
2872
+ const seen = /* @__PURE__ */ new Map();
2873
+ for (const m of fixtures) {
2874
+ if (m.stage !== "GROUP") continue;
2875
+ for (const t2 of [m.home, m.away]) {
2876
+ if (!/^[A-Z]{3}$/.test(t2.code) || t2.flag === "\u{1F3F3}\uFE0F") continue;
2877
+ if (!seen.has(t2.code)) seen.set(t2.code, { ...t2, group: m.group ?? void 0 });
2878
+ }
2879
+ }
2880
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
2881
+ }
2882
+ var TEAM_ALIASES = {
2883
+ turkey: "TUR",
2884
+ holland: "NED",
2885
+ korea: "KOR",
2886
+ skorea: "KOR",
2887
+ korearepublic: "KOR",
2888
+ republicofkorea: "KOR",
2889
+ czechrepublic: "CZE",
2890
+ congo: "COD",
2891
+ drcongo: "COD",
2892
+ drc: "COD",
2893
+ democraticrepublicofcongo: "COD",
2894
+ democraticrepublicofthecongo: "COD",
2895
+ cotedivoire: "CIV",
2896
+ caboverde: "CPV",
2897
+ bosnia: "BIH",
2898
+ bosniaandherzegovina: "BIH",
2899
+ us: "USA",
2900
+ america: "USA",
2901
+ unitedstatesofamerica: "USA"
2902
+ };
2903
+ function lookupTeam(query, fixtures = allFixtures()) {
2904
+ const roster = allTeams(fixtures);
2905
+ const q = norm2(query);
2906
+ if (!q) return { query, team: null, matches: [] };
2907
+ const byCode = roster.find((t2) => norm2(t2.code) === q);
2908
+ if (byCode) return { query, team: byCode, matches: [byCode] };
2909
+ const byName = roster.find((t2) => norm2(t2.name) === q);
2910
+ if (byName) return { query, team: byName, matches: [byName] };
2911
+ const aliasCode = TEAM_ALIASES[q];
2912
+ if (aliasCode) {
2913
+ const t2 = roster.find((r) => r.code === aliasCode);
2914
+ if (t2) return { query, team: t2, matches: [t2] };
2915
+ }
2916
+ if (q.length < 3) return { query, team: null, matches: [] };
2917
+ const prefix = roster.filter((t2) => norm2(t2.name).startsWith(q));
2918
+ const substr = roster.filter((t2) => norm2(t2.name).includes(q) && !prefix.includes(t2));
2919
+ const matches = [...prefix, ...substr];
2920
+ return { query, team: matches.length === 1 ? matches[0] : null, matches };
2921
+ }
2811
2922
  function blankRow(team) {
2812
2923
  return {
2813
2924
  team,
@@ -2833,6 +2944,7 @@ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2833
2944
  var DEFAULT_COMPETITION = "fifa.world";
2834
2945
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2835
2946
  var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
2947
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2836
2948
  function competitionBase(slug) {
2837
2949
  return `${ESPN_SOCCER}/${slug}`;
2838
2950
  }
@@ -2876,9 +2988,15 @@ function toInt(s) {
2876
2988
  return Number.isFinite(n) ? n : void 0;
2877
2989
  }
2878
2990
  function toTeam(t2) {
2879
- const name = t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD";
2880
- const code = (t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
2881
- return { code, name, flag: nationToFlag(t2?.displayName ?? t2?.abbreviation ?? name) };
2991
+ const name = sanitizeFeedText(
2992
+ t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
2993
+ );
2994
+ const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
2995
+ return {
2996
+ code,
2997
+ name,
2998
+ flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
2999
+ };
2882
3000
  }
2883
3001
  function mapEspnEvent(ev, ctx = {}) {
2884
3002
  const comp = ev.competitions?.[0];
@@ -2909,9 +3027,9 @@ function mapEspnEvent(ev, ctx = {}) {
2909
3027
  stage,
2910
3028
  group,
2911
3029
  kickoff: ev.date,
2912
- venue: comp?.venue?.fullName ?? "",
2913
- city: comp?.venue?.address?.city || void 0,
2914
- country: comp?.venue?.address?.country || void 0,
3030
+ venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3031
+ city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3032
+ country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
2915
3033
  home,
2916
3034
  away,
2917
3035
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3035,11 +3153,18 @@ var EspnAdapter = class {
3035
3153
  try {
3036
3154
  const res = await doFetch(url, {
3037
3155
  signal: controller.signal,
3156
+ // ESPN never legitimately redirects; following one would sidestep the
3157
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3158
+ redirect: "error",
3038
3159
  headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3039
3160
  });
3040
3161
  if (!res.ok) {
3041
3162
  throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3042
3163
  }
3164
+ const length = Number(res.headers?.get?.("content-length"));
3165
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3166
+ throw new Error(`ESPN response too large: ${length} bytes`);
3167
+ }
3043
3168
  return await res.json();
3044
3169
  } finally {
3045
3170
  clearTimeout(timer);
@@ -4195,12 +4320,19 @@ var PolymarketProvider = class {
4195
4320
  const doFetch = this.opts.fetchImpl ?? fetch;
4196
4321
  const res = await doFetch(url, {
4197
4322
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4323
+ // Gamma never legitimately redirects; following one would sidestep the
4324
+ // host allow-list (it only validates the base URL), so reject redirects.
4325
+ redirect: "error",
4198
4326
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4199
4327
  });
4200
4328
  if (res.status === 404) return void 0;
4201
4329
  if (!res.ok) {
4202
4330
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4203
4331
  }
4332
+ const length = Number(res.headers?.get?.("content-length"));
4333
+ if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4334
+ throw new Error(`Polymarket response too large: ${length} bytes`);
4335
+ }
4204
4336
  const data = await res.json();
4205
4337
  const event = Array.isArray(data) ? data[0] : data;
4206
4338
  return event && typeof event === "object" ? event : void 0;
@@ -4690,12 +4822,15 @@ var EN2 = {
4690
4822
  "next.none": "No upcoming fixture found for {team}.",
4691
4823
  "next.label": "Next up for {team}",
4692
4824
  "next.in": "in {countdown}",
4825
+ "team.group": "Group {group}",
4826
+ "team.ambiguous": '"{query}" is ambiguous. Did you mean:',
4827
+ "team.none": 'No team found for "{query}". Try a nation name or 3-letter code (e.g. Mexico, MEX).',
4828
+ "team.usage": "Usage: claudinho team <name or code> (e.g. claudinho team Mexico)",
4693
4829
  "table.title": "Group {group}",
4694
4830
  "table.none": "No group found for {group}.",
4695
4831
  "table.degraded": "Live standings unavailable \u2014 showing the group roster.",
4696
4832
  "table.empty": "No standings available.",
4697
4833
  "match.none": "No match found with id {id}.",
4698
- "status.scheduled": "scheduled",
4699
4834
  "status.live": "LIVE",
4700
4835
  "status.ht": "HT",
4701
4836
  "status.ft": "FT",
@@ -4724,12 +4859,15 @@ var ES2 = {
4724
4859
  "next.none": "No se encontr\xF3 pr\xF3ximo partido para {team}.",
4725
4860
  "next.label": "Pr\xF3ximo partido de {team}",
4726
4861
  "next.in": "en {countdown}",
4862
+ "team.group": "Grupo {group}",
4863
+ "team.ambiguous": '"{query}" es ambiguo. \xBFQuisiste decir:',
4864
+ "team.none": 'No se encontr\xF3 ning\xFAn equipo para "{query}". Prueba un nombre de pa\xEDs o un c\xF3digo de 3 letras (p. ej. Mexico, MEX).',
4865
+ "team.usage": "Uso: claudinho team <nombre o c\xF3digo> (p. ej. claudinho team Mexico)",
4727
4866
  "table.title": "Grupo {group}",
4728
4867
  "table.none": "No se encontr\xF3 el grupo {group}.",
4729
4868
  "table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
4730
4869
  "table.empty": "No hay clasificaci\xF3n disponible.",
4731
4870
  "match.none": "No se encontr\xF3 partido con id {id}.",
4732
- "status.scheduled": "programado",
4733
4871
  "status.live": "EN VIVO",
4734
4872
  "status.ht": "DESC",
4735
4873
  "status.ft": "FIN",
@@ -4758,12 +4896,15 @@ var PT2 = {
4758
4896
  "next.none": "Nenhum pr\xF3ximo jogo encontrado para {team}.",
4759
4897
  "next.label": "Pr\xF3ximo jogo de {team}",
4760
4898
  "next.in": "em {countdown}",
4899
+ "team.group": "Grupo {group}",
4900
+ "team.ambiguous": '"{query}" \xE9 amb\xEDguo. Voc\xEA quis dizer:',
4901
+ "team.none": 'Nenhuma sele\xE7\xE3o encontrada para "{query}". Tente um nome de pa\xEDs ou um c\xF3digo de 3 letras (ex. Mexico, MEX).',
4902
+ "team.usage": "Uso: claudinho team <nome ou c\xF3digo> (ex. claudinho team Mexico)",
4761
4903
  "table.title": "Grupo {group}",
4762
4904
  "table.none": "Grupo {group} n\xE3o encontrado.",
4763
4905
  "table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
4764
4906
  "table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
4765
4907
  "match.none": "Nenhum jogo encontrado com id {id}.",
4766
- "status.scheduled": "agendado",
4767
4908
  "status.live": "AO VIVO",
4768
4909
  "status.ht": "INT",
4769
4910
  "status.ft": "FIM",
@@ -4792,12 +4933,15 @@ var FR2 = {
4792
4933
  "next.none": "Aucun prochain match trouv\xE9 pour {team}.",
4793
4934
  "next.label": "Prochain match de {team}",
4794
4935
  "next.in": "dans {countdown}",
4936
+ "team.group": "Groupe {group}",
4937
+ "team.ambiguous": '"{query}" est ambigu. Vouliez-vous dire :',
4938
+ "team.none": 'Aucune \xE9quipe trouv\xE9e pour "{query}". Essayez un nom de pays ou un code \xE0 3 lettres (p. ex. Mexico, MEX).',
4939
+ "team.usage": "Usage : claudinho team <nom ou code> (p. ex. claudinho team Mexico)",
4795
4940
  "table.title": "Groupe {group}",
4796
4941
  "table.none": "Groupe {group} introuvable.",
4797
4942
  "table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
4798
4943
  "table.empty": "Aucun classement disponible.",
4799
4944
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
4800
- "status.scheduled": "pr\xE9vu",
4801
4945
  "status.live": "DIRECT",
4802
4946
  "status.ht": "MT",
4803
4947
  "status.ft": "FIN",
@@ -4820,7 +4964,7 @@ function makeT(lang) {
4820
4964
  const dict = CATALOGS2[lang] ?? EN2;
4821
4965
  return (key, vars) => {
4822
4966
  let s = dict[key] ?? EN2[key] ?? key;
4823
- if (vars) for (const [k, v] of Object.entries(vars)) s = s.replace(`{${k}}`, v);
4967
+ if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
4824
4968
  return s;
4825
4969
  };
4826
4970
  }
@@ -4879,7 +5023,7 @@ function matchLine(m, cfg, t2, c, flags = true) {
4879
5023
  const home = flags ? `${m.home.flag} ${m.home.name}` : m.home.name;
4880
5024
  const away = flags ? `${m.away.name} ${m.away.flag}` : m.away.name;
4881
5025
  const mid2 = isLive(m.status) || m.status === "FT" ? c.bold(scoreline(m)) : c.dim("vs");
4882
- const left = `${home.padEnd(22)} ${mid2.padStart(3)} ${away}`;
5026
+ const left = `${padVisible(home, 22)} ${mid2.padStart(3)} ${away}`;
4883
5027
  let right = "";
4884
5028
  if (m.status === "SCHEDULED") {
4885
5029
  right = c.dim(
@@ -5148,6 +5292,10 @@ function inLiveWindow(now = Date.now(), fixtures = allFixtures()) {
5148
5292
  function isResolvedFixture(m) {
5149
5293
  return isResolvedNation(m.home) && isResolvedNation(m.away);
5150
5294
  }
5295
+ function isMatchShaped(m) {
5296
+ const x = m;
5297
+ return !!x && typeof x === "object" && typeof x.id === "string" && typeof x.kickoff === "string" && !!x.home?.code && !!x.away?.code;
5298
+ }
5151
5299
  function nextOverall(now, fixtures = allFixtures()) {
5152
5300
  return [...fixtures].sort(byKickoff).find((m) => Date.parse(m.kickoff) >= now && isResolvedFixture(m));
5153
5301
  }
@@ -5168,7 +5316,7 @@ function liveMatchesFromCache(state, nowMs = Date.now()) {
5168
5316
  const liveArr = fresh && Array.isArray(state?.live) ? state.live : [];
5169
5317
  return liveArr.filter(
5170
5318
  (m) => !!m && typeof m === "object" && isLive(m.status) && !!m.home?.code && !!m.away?.code
5171
- );
5319
+ ).map(sanitizeMatchStrings);
5172
5320
  }
5173
5321
  function renderPrompt(state, opts = {}) {
5174
5322
  const now = opts.now ?? /* @__PURE__ */ new Date();
@@ -5177,7 +5325,7 @@ function renderPrompt(state, opts = {}) {
5177
5325
  const flags = opts.flags ?? true;
5178
5326
  const team = opts.team?.toUpperCase();
5179
5327
  const live = liveMatchesFromCache(state, nowMs);
5180
- const cachedFixtures = Array.isArray(state?.fixtures) ? state.fixtures : [];
5328
+ const cachedFixtures = Array.isArray(state?.fixtures) ? state.fixtures.filter(isMatchShaped).map(sanitizeMatchStrings) : [];
5181
5329
  const schedule = cachedFixtures.length ? mergeLive(allFixtures(), cachedFixtures) : void 0;
5182
5330
  if (team) {
5183
5331
  const mine = live.find((m) => m.home?.code === team || m.away?.code === team);
@@ -5210,10 +5358,16 @@ function renderPrompt(state, opts = {}) {
5210
5358
  }
5211
5359
 
5212
5360
  // src/hook.ts
5361
+ function rosterPinned(t2) {
5362
+ const { team } = lookupTeam(t2.code);
5363
+ return team ? { ...t2, name: team.name, flag: team.flag } : t2;
5364
+ }
5213
5365
  function line(m, flags) {
5214
5366
  const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
5215
- const home = flags ? `${m.home.flag} ${m.home.name}` : m.home.name;
5216
- const away = flags ? `${m.away.name} ${m.away.flag}` : m.away.name;
5367
+ const h = rosterPinned(m.home);
5368
+ const a = rosterPinned(m.away);
5369
+ const home = flags ? `${h.flag} ${h.name}` : h.name;
5370
+ const away = flags ? `${a.name} ${a.flag}` : a.name;
5217
5371
  return `${home} ${scoreline(m)} ${away} (${minute})`;
5218
5372
  }
5219
5373
  function renderHook(state, opts = {}) {
@@ -5329,7 +5483,10 @@ function spawnRefresh(source) {
5329
5483
  if (!entry) return;
5330
5484
  const child = spawn(process.execPath, [entry, "_refresh", "--source", source], {
5331
5485
  detached: true,
5332
- stdio: "ignore"
5486
+ stdio: "ignore",
5487
+ // Windows: a detached child gets its own console window without this —
5488
+ // the statusline would flash one every ~12–15s during live matches.
5489
+ windowsHide: true
5333
5490
  });
5334
5491
  child.unref();
5335
5492
  } catch {
@@ -5595,10 +5752,24 @@ function precheck(cfg, t2, date) {
5595
5752
  throw new InputError(t2("err.date", { date }));
5596
5753
  }
5597
5754
  }
5598
- function resolveTeamArg(team, usage) {
5599
- const code = team ?? process.env.CLAUDINHO_TEAM;
5600
- if (!code) throw new InputError(usage);
5601
- return code.toUpperCase();
5755
+ function resolveTeamArg(team, usage, t2) {
5756
+ const raw = team ?? process.env.CLAUDINHO_TEAM;
5757
+ if (!raw) throw new InputError(usage);
5758
+ const { team: hit, matches } = lookupTeam(raw);
5759
+ if (hit) return hit.code;
5760
+ if (matches.length > 1) {
5761
+ throw new InputError(
5762
+ `${t2("team.ambiguous", { query: raw })} ${matches.map((m) => `${m.name} (${m.code})`).join(", ")}`
5763
+ );
5764
+ }
5765
+ if (/^[A-Za-z]{3}$/.test(raw)) return raw.toUpperCase();
5766
+ throw new InputError(t2("team.none", { query: raw }));
5767
+ }
5768
+ function resolveEnvTeam(raw) {
5769
+ if (!raw) return void 0;
5770
+ const { team } = lookupTeam(raw);
5771
+ if (team) return team.code;
5772
+ return /^[A-Za-z]{3}$/.test(raw) ? raw.toUpperCase() : void 0;
5602
5773
  }
5603
5774
  async function cmdToday(date, ctx) {
5604
5775
  const { cfg, t: t2 } = ctx;
@@ -5670,7 +5841,7 @@ async function cmdLive(ctx) {
5670
5841
  async function cmdNext(team, ctx) {
5671
5842
  const { cfg, t: t2, now } = ctx;
5672
5843
  precheck(cfg, t2);
5673
- const code = resolveTeamArg(team, "Usage: claudinho next <team> (or set CLAUDINHO_TEAM)");
5844
+ const code = resolveTeamArg(team, "Usage: claudinho next <team> (or set CLAUDINHO_TEAM)", t2);
5674
5845
  const { fixture, degraded, source } = await getNextFixtureForTeam(
5675
5846
  adapterFor(ctx),
5676
5847
  code,
@@ -5692,7 +5863,7 @@ async function cmdNext(team, ctx) {
5692
5863
  out(header(t2("next.label", { team: code }), c));
5693
5864
  out();
5694
5865
  out(matchLine(fixture, cfg, t2, c, flags));
5695
- const stage = fixture.stage !== "GROUP" ? `${stageLabel(fixture)} \xB7 ` : "";
5866
+ const stage = fixture.stage !== "GROUP" ? `${stageLabelI18n(cfg.lang, fixture.stage)} \xB7 ` : "";
5696
5867
  out(
5697
5868
  " " + c.dim(
5698
5869
  `${stage}${formatKickoff(fixture.kickoff, { tz: cfg.tz, locale: cfg.lang })} \xB7 ` + t2("next.in", { countdown: countdown(fixture.kickoff) })
@@ -5704,6 +5875,37 @@ async function cmdNext(team, ctx) {
5704
5875
  out(disclaimer(t2, c));
5705
5876
  maybeStarNudge(ctx);
5706
5877
  }
5878
+ function cmdTeam(query, ctx) {
5879
+ const { cfg, t: t2 } = ctx;
5880
+ precheck(cfg, t2);
5881
+ const q = (query ?? "").trim();
5882
+ const { team, matches } = lookupTeam(q);
5883
+ if (cfg.json) {
5884
+ emitJson({ query: q, team: team ?? null, matches, count: matches.length });
5885
+ return;
5886
+ }
5887
+ const c = painterFor(cfg);
5888
+ const flags = flagsEnabled();
5889
+ const label = (tm) => {
5890
+ const flag = flags ? `${tm.flag} ` : "";
5891
+ const grp = tm.group ? ` \xB7 ${t2("team.group", { group: tm.group })}` : "";
5892
+ return ` ${flag}${c.bold(tm.name)} ${c.dim(tm.code + grp)}`;
5893
+ };
5894
+ out();
5895
+ if (!q) {
5896
+ out(" " + c.dim(t2("team.usage")));
5897
+ } else if (team) {
5898
+ out(label(team));
5899
+ } else if (matches.length > 0) {
5900
+ out(" " + c.dim(t2("team.ambiguous", { query: q })));
5901
+ for (const m of matches) out(label(m));
5902
+ } else {
5903
+ out(" " + c.dim(t2("team.none", { query: q })));
5904
+ }
5905
+ out();
5906
+ out(disclaimer(t2, c));
5907
+ maybeStarNudge(ctx);
5908
+ }
5707
5909
  async function cmdTable(group, ctx) {
5708
5910
  const { cfg, t: t2 } = ctx;
5709
5911
  precheck(cfg, t2);
@@ -5822,7 +6024,7 @@ async function cmdBracket(stage, opts, ctx) {
5822
6024
  function cmdPrompt({ cfg }) {
5823
6025
  try {
5824
6026
  const payload = readCursorPayload();
5825
- const team = process.env.CLAUDINHO_TEAM;
6027
+ const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
5826
6028
  const compact = !["0", "false", "no"].includes(
5827
6029
  (process.env.CLAUDINHO_COMPACT ?? "").toLowerCase()
5828
6030
  );
@@ -5840,7 +6042,7 @@ function cmdPrompt({ cfg }) {
5840
6042
  }
5841
6043
  function cmdHook({ cfg }) {
5842
6044
  try {
5843
- const team = process.env.CLAUDINHO_TEAM;
6045
+ const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
5844
6046
  const state = readCurrentState(cfg.source, resolveCompetition());
5845
6047
  const ctx = renderHook(state, { team, flags: flagsEnabled() });
5846
6048
  if (ctx) out(ctx);
@@ -5892,7 +6094,8 @@ function cmdInitCursor(opts, { cfg }) {
5892
6094
  out(CURSOR_MCP_SNIPPET);
5893
6095
  return;
5894
6096
  }
5895
- printInitResult(initCursorStatusline(), cfg);
6097
+ const res = initCursorStatusline();
6098
+ printInitResult(res, cfg);
5896
6099
  out("");
5897
6100
  out("Optional \u2014 live MCP tools in Cursor: add to ~/.cursor/mcp.json (or project .cursor/mcp.json):");
5898
6101
  out(CURSOR_MCP_SNIPPET);
@@ -5900,7 +6103,7 @@ function cmdInitCursor(opts, { cfg }) {
5900
6103
  out("Tip: export CLAUDINHO_CURSOR_META=auto for a model + context line below the score.");
5901
6104
  out("");
5902
6105
  out("\u2192 Restart your agent session to see it.");
5903
- printInitStarCta(cfg);
6106
+ if (res.action === "written") printInitStarCta(cfg);
5904
6107
  }
5905
6108
  function cmdInitClaude(opts, { cfg }) {
5906
6109
  if (opts.print) {
@@ -5914,14 +6117,16 @@ function cmdInitClaude(opts, { cfg }) {
5914
6117
  out(CLAUDE_MCP_ONELINER);
5915
6118
  return;
5916
6119
  }
5917
- printInitResult(initStatusline(), cfg);
5918
- printInitResult(initHook(), cfg);
6120
+ const statusRes = initStatusline();
6121
+ const hookRes = initHook();
6122
+ printInitResult(statusRes, cfg);
6123
+ printInitResult(hookRes, cfg);
5919
6124
  out("");
5920
6125
  out("Next \u2014 add the MCP server:");
5921
6126
  out(` ${CLAUDE_MCP_ONELINER}`);
5922
6127
  out("");
5923
6128
  out("\u2192 Restart Claude Code to see it.");
5924
- printInitStarCta(cfg);
6129
+ if (statusRes.action === "written" || hookRes.action === "written") printInitStarCta(cfg);
5925
6130
  }
5926
6131
  async function cmdMatch(id, ctx) {
5927
6132
  const { cfg, t: t2 } = ctx;
@@ -5945,7 +6150,7 @@ async function cmdMatch(id, ctx) {
5945
6150
  out(disclaimer(t2, c));
5946
6151
  return;
5947
6152
  }
5948
- const stageLabelText = stageLabel(match);
6153
+ const stageLabelText = stageLabelI18n(cfg.lang, match.stage, match.group ?? void 0);
5949
6154
  out(header(`${match.home.name} ${scoreline(match)} ${match.away.name}`, c));
5950
6155
  out(" " + c.dim(`${stageLabelText} \xB7 ${matchLocation(match)}`));
5951
6156
  out(
@@ -5991,7 +6196,7 @@ async function cmdMarkets(target, team, ctx) {
5991
6196
  const { cfg, t: t2 } = ctx;
5992
6197
  if (target === "next") {
5993
6198
  precheck(cfg, t2);
5994
- const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)");
6199
+ const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)", t2);
5995
6200
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
5996
6201
  const { match: fixture, degraded } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
5997
6202
  const sig = fixture && marketRelevant(fixture, now2) ? (await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS)).get(fixture.id) : void 0;
@@ -6267,7 +6472,7 @@ async function cmdShare(target, team, opts, ctx) {
6267
6472
  }
6268
6473
  if (target === "next") {
6269
6474
  precheck(cfg, t2);
6270
- const code = resolveTeamArg(team, "Usage: claudinho share next <team> (or set CLAUDINHO_TEAM)");
6475
+ const code = resolveTeamArg(team, "Usage: claudinho share next <team> (or set CLAUDINHO_TEAM)", t2);
6271
6476
  const { fixture, degraded: degraded2, source: source2 } = await getNextFixtureForTeam(
6272
6477
  adapterFor(ctx),
6273
6478
  code,
@@ -6419,6 +6624,7 @@ function maybeStarNudge(ctx) {
6419
6624
  out(c.dim(` \u2B50 Enjoying Claudinho? Star it \u2192 ${REPO_URL} (claudinho star)`));
6420
6625
  }
6421
6626
  function printInitStarCta(cfg) {
6627
+ if (cfg.json || !process.stdout.isTTY || process.env.CLAUDINHO_NO_STAR) return;
6422
6628
  const c = painterFor(cfg);
6423
6629
  out("");
6424
6630
  out(c.dim(`\u2B50 If this keeps you in the flow during the match, star the repo \u2192 ${REPO_URL}`));
@@ -6432,7 +6638,8 @@ function cmdVibe(ctx) {
6432
6638
  const state = readCurrentState(cfg.source, resolveCompetition());
6433
6639
  liveSeg = vibeLiveSegment(
6434
6640
  liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()),
6435
- process.env.CLAUDINHO_TEAM
6641
+ // Name-or-code, matching the statusline/hook (offline lookup).
6642
+ resolveEnvTeam(process.env.CLAUDINHO_TEAM)
6436
6643
  );
6437
6644
  } catch {
6438
6645
  }
@@ -6456,7 +6663,7 @@ function handlePipeError(stream) {
6456
6663
  }
6457
6664
  handlePipeError(process.stdout);
6458
6665
  handlePipeError(process.stderr);
6459
- var VERSION = "0.8.17";
6666
+ var VERSION = "0.8.19";
6460
6667
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
6461
6668
  function ctxFrom(cmd) {
6462
6669
  let root = cmd;
@@ -6490,13 +6697,20 @@ program.command("live").description("show matches in play right now").action(asy
6490
6697
  fail(e);
6491
6698
  }
6492
6699
  });
6493
- program.command("next").description("show a team's next fixture").argument("[team]", "team code, e.g. MEX (default: $CLAUDINHO_TEAM)").action(async (team, _opts, cmd) => {
6700
+ program.command("next").description("show a team's next fixture").argument("[team]", "team name or code, e.g. Mexico or MEX (default: $CLAUDINHO_TEAM)").action(async (team, _opts, cmd) => {
6494
6701
  try {
6495
6702
  await cmdNext(team, ctxFrom(cmd));
6496
6703
  } catch (e) {
6497
6704
  fail(e);
6498
6705
  }
6499
6706
  });
6707
+ program.command("team").description("resolve a nation name or code to its FIFA code, flag, and group").argument("<query>", 'team name or 3-letter code, e.g. Mexico, MEX, "DR Congo"').action((query, _opts, cmd) => {
6708
+ try {
6709
+ cmdTeam(query, ctxFrom(cmd));
6710
+ } catch (e) {
6711
+ fail(e);
6712
+ }
6713
+ });
6500
6714
  program.command("table").description("show group standings (default: all groups)").argument("[group]", "group letter A-L").action(async (group, _opts, cmd) => {
6501
6715
  try {
6502
6716
  await cmdTable(group, ctxFrom(cmd));
@@ -6518,7 +6732,7 @@ program.command("match").description("show a single match by id").argument("<id>
6518
6732
  fail(e);
6519
6733
  }
6520
6734
  });
6521
- program.command("markets").description("show prediction-market signals (read-only, informational only)").argument("[target]", 'date (YYYY-MM-DD), match id, "today", or "next"').argument("[team]", 'team code when target is "next" (default: $CLAUDINHO_TEAM)').action(async (target, team, _opts, cmd) => {
6735
+ program.command("markets").description("show prediction-market signals (read-only, informational only)").argument("[target]", 'date (YYYY-MM-DD), match id, "today", or "next"').argument("[team]", 'team name or code when target is "next", e.g. Mexico or MEX (default: $CLAUDINHO_TEAM)').action(async (target, team, _opts, cmd) => {
6522
6736
  try {
6523
6737
  await cmdMarkets(target, team, ctxFrom(cmd));
6524
6738
  } catch (e) {
@@ -6527,7 +6741,7 @@ program.command("markets").description("show prediction-market signals (read-onl
6527
6741
  });
6528
6742
  program.command("share").description("print a shareable, copy-pasteable match snippet (#VibingLaVidaLoca)").argument("[target]", '"today" (default), "live", a date, a match id, "next", "table", or "bracket"').argument(
6529
6743
  "[team]",
6530
- 'team code for "next" (default: $CLAUDINHO_TEAM), group letter for "table", or stage for "bracket"'
6744
+ 'team name or code for "next" (default: $CLAUDINHO_TEAM), group letter for "table", or stage for "bracket"'
6531
6745
  ).option("--style <style>", "snippet style: social (default) or compact").option("--copy", "also copy the snippet to the clipboard (best-effort)").option("--no-hashtag", "omit the #VibingLaVidaLoca tag").option("--no-install-line", "omit the install/run cue").action(async (target, team, opts, cmd) => {
6532
6746
  try {
6533
6747
  await cmdShare(target, team, opts, ctxFrom(cmd));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.8.17",
3
+ "version": "0.8.19",
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",
@@ -61,7 +61,7 @@
61
61
  "tsup": "^8.0.0",
62
62
  "typescript": "^5.7.0",
63
63
  "vitest": "^4.1.9",
64
- "@claudinho/core": "0.8.17"
64
+ "@claudinho/core": "0.8.19"
65
65
  },
66
66
  "scripts": {
67
67
  "build": "tsup",