@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.
- package/README.md +3 -2
- package/dist/index.js +252 -38
- 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
|
|
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 =
|
|
2880
|
-
|
|
2881
|
-
|
|
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.
|
|
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
|
|
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
|
|
5216
|
-
const
|
|
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
|
|
5600
|
-
if (!
|
|
5601
|
-
|
|
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" ? `${
|
|
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
|
-
|
|
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
|
-
|
|
5918
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
+
"@claudinho/core": "0.8.19"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "tsup",
|