@claudinho/cli 0.4.2 → 0.5.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 +17 -18
  2. package/dist/index.js +345 -95
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -20,11 +20,12 @@ npx @claudinho/cli today
20
20
  ```bash
21
21
  claudinho today [date] # a day's fixtures in your timezone (default: today), live scores inline
22
22
  claudinho live # matches in play right now
23
- claudinho next <TEAM> # a team's next fixture + countdown (e.g. next MEX)
24
- claudinho table [GROUP] # group standings (default: all groups)
23
+ claudinho next [TEAM] # a team's next fixture + countdown (default: $CLAUDINHO_TEAM)
24
+ claudinho table [GROUP] # live cumulative group standings (default: all groups)
25
25
  claudinho match <id> # a single match's detail
26
26
  claudinho markets [target] # prediction-market signals: today | <date> | <id> | next <TEAM>
27
- claudinho share [target] # copy-pasteable match snippet: today | live | <date> | <id> | next <TEAM>
27
+ # (next prefers the team's IN-PLAY match while one is live)
28
+ claudinho share [target] # copy-pasteable snippet: today | live | <date> | <id> | next <TEAM> | table <GROUP>
28
29
  claudinho prompt # one compact status line (for statusline/tmux/Starship)
29
30
  claudinho init-statusline # wire it into the Claude Code statusline
30
31
  claudinho hook # live-score context for a Claude Code hook (silent off-match)
@@ -77,7 +78,7 @@ market-implied percentages — for a date, a match, or a team's next fixture:
77
78
  claudinho markets # today's signals
78
79
  claudinho markets 2026-06-11 # a specific date
79
80
  claudinho markets 760415 # one match by id
80
- claudinho markets next MEX # a team's next fixture
81
+ claudinho markets next MEX # a team's current-or-next fixture (in-play preferred)
81
82
  claudinho markets today --json # structured sidecar output
82
83
  ```
83
84
 
@@ -91,11 +92,12 @@ Opt out with `--no-markets` (per command) or `CLAUDINHO_MARKETS=off` (global). T
91
92
  statusline and hook **never** show market data — it stays off the hot path.
92
93
 
93
94
  > **How matches are matched:** event slugs are derived automatically from each
94
- > fixture (`fifwc-{home}-{away}-{date}`), so real odds appear for any match with a
95
+ > fixture (`fifwc-{home}-{away}-{date}`), so real signals appear for any match with a
95
96
  > live Polymarket market — no mapping needed (`mapping.2026.json` is for slug
96
97
  > *overrides* only). Matching fails closed, so an unmatched fixture simply shows
97
- > nothing. For an offline preview, set `CLAUDINHO_MARKETS_SOURCE=fake` to render
98
- > clearly-labeled synthetic **"demo data"** odds.
98
+ > nothing and finished matches never show one (market signals are pre-match
99
+ > and in-play reads). For an offline preview, set `CLAUDINHO_MARKETS_SOURCE=fake`
100
+ > to render clearly-labeled synthetic **"demo data"** signals.
99
101
 
100
102
  ## Shareable snippets
101
103
 
@@ -106,26 +108,23 @@ social posts, READMEs, and issue comments — your terminal football, ready to p
106
108
  claudinho share # today's matches
107
109
  claudinho share live # matches in play
108
110
  claudinho share next MEX # a team's next fixture (+ market read, when reliable)
111
+ claudinho share table A # a group's standings card (facts only, no market line)
109
112
  claudinho share 760415 # one match by id
110
113
  claudinho share next MEX --copy # …and copy it straight to the clipboard
111
114
  ```
112
115
 
113
- <!-- DEMO CARD: verbatim output of `claudinho share next MEX --tz America/Mexico_City`.
116
+ <!-- DEMO CARD: verbatim output of `claudinho share next USA --tz America/Los_Angeles`.
114
117
  REGENERATE immediately before merging — the market block is gate-conditional
115
118
  and the numbers drift. Never hand-edit. -->
116
119
  ```text
117
- Next up for Mexico
120
+ Next up for United States
118
121
 
119
- 🇲🇽 Mexico vs South Africa 🇿🇦
120
- Jun 11 · 13:00 America/Mexico_City
121
- Estadio Banorte, Mexico City, Mexico
122
-
123
- Prediction markets favor Mexico.
124
- Mexico 69% · Draw 20% · South Africa 10%
125
- Source: Polymarket · updated 08:15 UTC · informational only
122
+ 🇺🇸 United States vs Paraguay 🇵🇾
123
+ Jun 12 · 18:00 America/Los_Angeles
124
+ SoFi Stadium, Inglewood, California, USA
126
125
 
127
126
  #VibingLaVidaLoca · Independent fan project · not affiliated with FIFA or Anthropic.
128
- Try it: npx @claudinho/cli next MEX
127
+ Try it: npx @claudinho/cli next USA
129
128
  ```
130
129
 
131
130
  Snippets are **plain text** (no color codes — they paste cleanly everywhere) and
@@ -161,7 +160,7 @@ The statusline reads from a local micro-cache and **never blocks on the
161
160
  network** (<150ms). When several matches are live it shows them all inline:
162
161
  `⚽ 🇳🇴 1–1 🇫🇷 87' · 🇸🇳 1–2 🇮🇶 86'`. Customize via env:
163
162
 
164
- - `CLAUDINHO_TEAM=MEX` — show only your team's match
163
+ - `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
165
164
  - `CLAUDINHO_MAX=2` — cap how many live matches show inline (rest collapse to `+N`; default: all)
166
165
  - `CLAUDINHO_COMPACT=0` — show 3-letter codes alongside flags
167
166
 
package/dist/index.js CHANGED
@@ -2610,6 +2610,13 @@ function nextFixtureForTeam(code, opts = {}) {
2610
2610
  (m) => new Date(m.kickoff).getTime() >= from.getTime()
2611
2611
  );
2612
2612
  }
2613
+ var LIVE_WINDOW_MS = 140 * 6e4;
2614
+ function fixturesInLiveWindow(now = Date.now(), fixtures = SCHEDULE) {
2615
+ return fixtures.filter((m) => {
2616
+ const k = Date.parse(m.kickoff);
2617
+ return now >= k && now <= k + LIVE_WINDOW_MS;
2618
+ }).sort(byKickoff);
2619
+ }
2613
2620
  function groups(fixtures = SCHEDULE) {
2614
2621
  const set = /* @__PURE__ */ new Set();
2615
2622
  for (const m of fixtures) if (m.group) set.add(m.group);
@@ -2755,6 +2762,43 @@ function mapEspnEvent(ev, ctx = {}) {
2755
2762
  function toEspnDate(d) {
2756
2763
  return d.replace(/\D/g, "").slice(0, 8);
2757
2764
  }
2765
+ function statVal(stats, name) {
2766
+ const v = stats?.find((s) => s.name === name)?.value;
2767
+ return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
2768
+ }
2769
+ function entryToRow(e) {
2770
+ return {
2771
+ team: toTeam(e.team),
2772
+ played: statVal(e.stats, "gamesPlayed"),
2773
+ won: statVal(e.stats, "wins"),
2774
+ drawn: statVal(e.stats, "ties"),
2775
+ lost: statVal(e.stats, "losses"),
2776
+ goalsFor: statVal(e.stats, "pointsFor"),
2777
+ goalsAgainst: statVal(e.stats, "pointsAgainst"),
2778
+ goalDiff: statVal(e.stats, "pointDifferential"),
2779
+ points: statVal(e.stats, "points")
2780
+ };
2781
+ }
2782
+ function parseStandings(data) {
2783
+ const out2 = [];
2784
+ for (const child of data.children ?? []) {
2785
+ const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
2786
+ if (!letter) continue;
2787
+ const ranked = (child.standings?.entries ?? []).map((e) => ({
2788
+ row: entryToRow(e),
2789
+ rank: statVal(e.stats, "rank")
2790
+ }));
2791
+ ranked.sort((a, b) => {
2792
+ if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
2793
+ const r = a.row;
2794
+ const s = b.row;
2795
+ return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
2796
+ });
2797
+ out2.push({ group: letter, rows: ranked.map((x) => x.row) });
2798
+ }
2799
+ out2.sort((a, b) => a.group.localeCompare(b.group));
2800
+ return out2;
2801
+ }
2758
2802
  var EspnAdapter = class {
2759
2803
  constructor(opts = {}) {
2760
2804
  this.opts = opts;
@@ -2774,25 +2818,30 @@ var EspnAdapter = class {
2774
2818
  const today = await this.fetchScoreboard();
2775
2819
  return today.filter((m) => m.status === "LIVE" || m.status === "HT");
2776
2820
  }
2821
+ /** Standings endpoint URL (lives under apis/v2, not site/v2; derived from base). */
2822
+ standingsUrl() {
2823
+ const base = this.opts.baseUrl ?? DEFAULT_BASE;
2824
+ return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
2825
+ }
2826
+ /**
2827
+ * Authoritative, cumulative group tables from the standings endpoint. Throws
2828
+ * on fetch/parse failure (the caller decides the fallback). Group-stage only:
2829
+ * non-group `children` are filtered out by {@link parseStandings}.
2830
+ */
2831
+ async fetchStandings() {
2832
+ return parseStandings(await this.get(this.standingsUrl()));
2833
+ }
2777
2834
  /**
2778
2835
  * Build (and cache) a team-code -> group-letter map from the standings
2779
- * endpoint. Best-effort: returns {} if standings are unavailable.
2836
+ * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
2837
+ * same parse as {@link fetchStandings}, so the two never drift.
2780
2838
  */
2781
2839
  async fetchGroupMap(force = false) {
2782
2840
  if (this.groupMap && !force) return this.groupMap;
2783
- const base = this.opts.baseUrl ?? DEFAULT_BASE;
2784
- const standingsUrl = `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
2785
2841
  const map = {};
2786
2842
  try {
2787
- const data = await this.get(standingsUrl);
2788
- for (const child of data.children ?? []) {
2789
- const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
2790
- if (!letter) continue;
2791
- for (const e of child.standings?.entries ?? []) {
2792
- const code = e.team?.abbreviation?.toUpperCase();
2793
- if (code) map[code] = letter;
2794
- }
2795
- }
2843
+ const tables = parseStandings(await this.get(this.standingsUrl()));
2844
+ for (const t of tables) for (const r of t.rows) map[r.team.code] = t.group;
2796
2845
  } catch {
2797
2846
  }
2798
2847
  this.groupMap = map;
@@ -2863,10 +2912,53 @@ async function getMatchesForDate(adapter, dateISO) {
2863
2912
  return { matches: base, degraded: true };
2864
2913
  }
2865
2914
  }
2915
+ async function getStandings(adapter, group) {
2916
+ const want = group?.toUpperCase();
2917
+ if (adapter.fetchStandings) {
2918
+ try {
2919
+ const all = await adapter.fetchStandings();
2920
+ const tables2 = (want ? all.filter((t) => t.group === want) : all).sort(
2921
+ (a, b) => a.group.localeCompare(b.group)
2922
+ );
2923
+ return { tables: tables2, degraded: false, source: adapter.name };
2924
+ } catch {
2925
+ }
2926
+ }
2927
+ const letters = want ? [want] : groups();
2928
+ const tables = letters.map((g) => ({ group: g, rows: computeStandings(fixturesByGroup(g)) })).filter((t) => t.rows.length > 0);
2929
+ return { tables, degraded: true };
2930
+ }
2866
2931
  function shiftUtcDate(dateISO, days) {
2867
2932
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
2868
2933
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
2869
2934
  }
2935
+ var EXTRA_TIME_SLACK_MS = 60 * 6e4;
2936
+ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
2937
+ const nowMs = now.getTime();
2938
+ const candidate = fixturesByTeam(code).find((m) => {
2939
+ const k = Date.parse(m.kickoff);
2940
+ return nowMs >= k && nowMs <= k + LIVE_WINDOW_MS + EXTRA_TIME_SLACK_MS;
2941
+ });
2942
+ if (candidate) {
2943
+ const r = await getMatchById(adapter, candidate.id);
2944
+ const m = r.match ?? candidate;
2945
+ if (!isFinished(m.status)) return { ...r, match: m };
2946
+ }
2947
+ const next = nextFixtureForTeam(code, { from: now });
2948
+ return { match: next, degraded: false };
2949
+ }
2950
+ async function getMatchById(adapter, id) {
2951
+ const base = allFixtures().find((m) => m.id === id);
2952
+ if (!base) return { match: void 0, degraded: false };
2953
+ const day = base.kickoff.slice(0, 10);
2954
+ try {
2955
+ const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
2956
+ const hit = live.find((m) => m.id === id);
2957
+ return { match: hit ?? base, degraded: false, source: hit ? adapter.name : void 0 };
2958
+ } catch {
2959
+ return { match: base, degraded: true };
2960
+ }
2961
+ }
2870
2962
  async function getLiveMatches(adapter) {
2871
2963
  try {
2872
2964
  return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
@@ -2875,6 +2967,12 @@ async function getLiveMatches(adapter) {
2875
2967
  }
2876
2968
  }
2877
2969
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
2970
+ function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
2971
+ if (isLive(match.status)) return true;
2972
+ if (isFinished(match.status)) return false;
2973
+ const k = Date.parse(match.kickoff);
2974
+ return !Number.isFinite(k) || now.getTime() <= k + LIVE_WINDOW_MS;
2975
+ }
2878
2976
  function normalizeOutcomes(outcomes) {
2879
2977
  const sum = outcomes.reduce(
2880
2978
  (s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
@@ -3362,11 +3460,50 @@ function formatShareSnippet(input, options = {}) {
3362
3460
  blocks.push(card.join("\n"));
3363
3461
  }
3364
3462
  }
3463
+ blocks.push(
3464
+ shareFooter({
3465
+ source: input.source,
3466
+ installLine: input.installLine,
3467
+ includeHashtag,
3468
+ includeInstall
3469
+ })
3470
+ );
3471
+ return blocks.join("\n\n");
3472
+ }
3473
+ function shareFooter(opts) {
3365
3474
  const footer = [];
3366
- if (input.source) footer.push(`Live data: ${liveSourceLabel(input.source)}`);
3367
- footer.push([includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 "));
3368
- if (includeInstall && input.installLine) footer.push(`Try it: ${input.installLine}`);
3369
- blocks.push(footer.join("\n"));
3475
+ if (opts.source) footer.push(`Live data: ${liveSourceLabel(opts.source)}`);
3476
+ footer.push(
3477
+ [opts.includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 ")
3478
+ );
3479
+ if (opts.includeInstall && opts.installLine) footer.push(`Try it: ${opts.installLine}`);
3480
+ return footer.join("\n");
3481
+ }
3482
+ function gd(n) {
3483
+ return n > 0 ? `+${n}` : `${n}`;
3484
+ }
3485
+ function tableRow(r, rank) {
3486
+ return `${rank}. ${r.team.flag} ${r.team.code} ${r.points} pts \xB7 ${r.won}-${r.drawn}-${r.lost} \xB7 ${gd(r.goalDiff)}`;
3487
+ }
3488
+ function formatShareTable(input, options = {}) {
3489
+ const includeHashtag = options.includeHashtag !== false;
3490
+ const includeInstall = options.includeInstallLine !== false;
3491
+ const blocks = [];
3492
+ if (input.tables.length === 0) {
3493
+ blocks.push(input.emptyNote ?? "No standings available.");
3494
+ } else {
3495
+ for (const { group, rows } of input.tables) {
3496
+ blocks.push(
3497
+ [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
3498
+ );
3499
+ }
3500
+ if (input.degraded) {
3501
+ blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
3502
+ }
3503
+ }
3504
+ blocks.push(
3505
+ shareFooter({ source: input.source, installLine: input.installLine, includeHashtag, includeInstall })
3506
+ );
3370
3507
  return blocks.join("\n\n");
3371
3508
  }
3372
3509
 
@@ -3425,6 +3562,8 @@ var EN = {
3425
3562
  "next.in": "in {countdown}",
3426
3563
  "table.title": "Group {group}",
3427
3564
  "table.none": "No group found for {group}.",
3565
+ "table.degraded": "Live standings unavailable \u2014 showing the group roster.",
3566
+ "table.empty": "No standings available.",
3428
3567
  "match.none": "No match found with id {id}.",
3429
3568
  "status.scheduled": "scheduled",
3430
3569
  "status.live": "LIVE",
@@ -3455,6 +3594,8 @@ var ES = {
3455
3594
  "next.in": "en {countdown}",
3456
3595
  "table.title": "Grupo {group}",
3457
3596
  "table.none": "No se encontr\xF3 el grupo {group}.",
3597
+ "table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
3598
+ "table.empty": "No hay clasificaci\xF3n disponible.",
3458
3599
  "match.none": "No se encontr\xF3 partido con id {id}.",
3459
3600
  "status.scheduled": "programado",
3460
3601
  "status.live": "EN VIVO",
@@ -3485,6 +3626,8 @@ var PT = {
3485
3626
  "next.in": "em {countdown}",
3486
3627
  "table.title": "Grupo {group}",
3487
3628
  "table.none": "Grupo {group} n\xE3o encontrado.",
3629
+ "table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
3630
+ "table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
3488
3631
  "match.none": "Nenhum jogo encontrado com id {id}.",
3489
3632
  "status.scheduled": "agendado",
3490
3633
  "status.live": "AO VIVO",
@@ -3515,6 +3658,8 @@ var FR = {
3515
3658
  "next.in": "dans {countdown}",
3516
3659
  "table.title": "Groupe {group}",
3517
3660
  "table.none": "Groupe {group} introuvable.",
3661
+ "table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
3662
+ "table.empty": "Aucun classement disponible.",
3518
3663
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
3519
3664
  "status.scheduled": "pr\xE9vu",
3520
3665
  "status.live": "DIRECT",
@@ -3812,15 +3957,10 @@ function releaseLock() {
3812
3957
  }
3813
3958
 
3814
3959
  // src/statusline.ts
3815
- var LIVE_WINDOW_MS = 140 * 6e4;
3816
3960
  var DISPLAY_STALE_MS = 5 * 6e4;
3817
3961
  var LIVE_TTL_MS = 15e3;
3818
3962
  function inLiveWindow(now = Date.now(), fixtures = allFixtures()) {
3819
- for (const m of fixtures) {
3820
- const k = Date.parse(m.kickoff);
3821
- if (now >= k && now <= k + LIVE_WINDOW_MS) return true;
3822
- }
3823
- return false;
3963
+ return fixturesInLiveWindow(now, fixtures).length > 0;
3824
3964
  }
3825
3965
  function nextOverall(now, fixtures = allFixtures()) {
3826
3966
  return [...fixtures].sort(byKickoff).find((m) => Date.parse(m.kickoff) >= now);
@@ -3855,6 +3995,17 @@ function renderPrompt(state, opts = {}) {
3855
3995
  if (overflow > 0) line2 += ` +${overflow}`;
3856
3996
  return line2;
3857
3997
  }
3998
+ const cacheFresh = !!state && state.degraded !== true && ageMs(state, nowMs) < DISPLAY_STALE_MS;
3999
+ if (!cacheFresh) {
4000
+ const win = fixturesInLiveWindow(nowMs).filter(
4001
+ (m) => !team || m.home.code === team || m.away.code === team
4002
+ );
4003
+ const first = win[0];
4004
+ if (first) {
4005
+ const more = win.length - 1;
4006
+ return `\u26BD ${first.home.flag} vs ${first.away.flag} live \xB7 syncing\u2026` + (more > 0 ? ` +${more}` : "");
4007
+ }
4008
+ }
3858
4009
  const next = team ? nextFixtureForTeam(team, { from: now }) : nextOverall(nowMs);
3859
4010
  if (next) {
3860
4011
  return `${next.home.flag} vs ${next.away.flag} in ${countdown(next.kickoff, now)}`;
@@ -4070,8 +4221,10 @@ async function marketSignalsFor(ctx, matches, opts = {}) {
4070
4221
  }
4071
4222
  async function reliableMarketSignals(ctx, matches) {
4072
4223
  if (ctx.cfg.markets === false) return /* @__PURE__ */ new Map();
4073
- const raw = await marketSignalsFor(ctx, matches, DEFAULT_ON_MARKET_OPTS);
4074
- const now = /* @__PURE__ */ new Date();
4224
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
4225
+ const relevant = matches.filter((m) => marketRelevant(m, now));
4226
+ if (relevant.length === 0) return /* @__PURE__ */ new Map();
4227
+ const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS);
4075
4228
  const out2 = /* @__PURE__ */ new Map();
4076
4229
  for (const [id, s] of raw) if (isReliableMarketSignal(s, { now })) out2.set(id, s);
4077
4230
  return out2;
@@ -4094,10 +4247,22 @@ function precheck(cfg, t, date) {
4094
4247
  if (cfg.tz && !isValidTimeZone(cfg.tz)) {
4095
4248
  process.stderr.write(t("warn.tz", { tz: cfg.tz }) + "\n");
4096
4249
  }
4250
+ const competition = resolveCompetition();
4251
+ if (competition !== DEFAULT_COMPETITION) {
4252
+ process.stderr.write(
4253
+ `claudinho: CLAUDINHO_COMPETITION=${competition} \u2014 live data follows a different competition than the bundled 2026 schedule.
4254
+ `
4255
+ );
4256
+ }
4097
4257
  if (date !== void 0 && !isValidDate(date)) {
4098
4258
  throw new InputError(t("err.date", { date }));
4099
4259
  }
4100
4260
  }
4261
+ function resolveTeamArg(team, usage) {
4262
+ const code = team ?? process.env.CLAUDINHO_TEAM;
4263
+ if (!code) throw new InputError(usage);
4264
+ return code.toUpperCase();
4265
+ }
4101
4266
  async function cmdToday(date, ctx) {
4102
4267
  const { cfg, t } = ctx;
4103
4268
  precheck(cfg, t, date);
@@ -4158,10 +4323,10 @@ async function cmdLive(ctx) {
4158
4323
  if (src) out(src);
4159
4324
  out(disclaimer(t, c));
4160
4325
  }
4161
- async function cmdNext(team, { cfg, t }) {
4326
+ async function cmdNext(team, { cfg, t, now }) {
4162
4327
  precheck(cfg, t);
4163
- const code = team.toUpperCase();
4164
- const fixture = nextFixtureForTeam(code);
4328
+ const code = resolveTeamArg(team, "Usage: claudinho next <team> (or set CLAUDINHO_TEAM)");
4329
+ const fixture = nextFixtureForTeam(code, { from: now ?? /* @__PURE__ */ new Date() });
4165
4330
  if (cfg.json) {
4166
4331
  emitJson({ team: code, fixture: fixture ?? null });
4167
4332
  return;
@@ -4188,32 +4353,30 @@ async function cmdNext(team, { cfg, t }) {
4188
4353
  async function cmdTable(group, ctx) {
4189
4354
  const { cfg, t } = ctx;
4190
4355
  precheck(cfg, t);
4191
- const adapter = adapterFor(ctx);
4192
- const { matches, degraded, source } = await getMatchesForDate(
4193
- adapter,
4194
- localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz)
4195
- );
4196
- const wanted = group ? [group.toUpperCase()] : groups(matches);
4356
+ const { tables, degraded, source } = await getStandings(adapterFor(ctx), group);
4197
4357
  if (cfg.json) {
4198
- const tables = wanted.map((g) => ({
4199
- group: g,
4200
- standings: computeStandings(fixturesByGroup(g, matches))
4201
- }));
4358
+ const json = tables.map((tb) => ({ group: tb.group, standings: tb.rows }));
4202
4359
  emitJson({
4203
4360
  degraded,
4204
4361
  source: source ?? null,
4205
- tables: group ? tables[0] ?? null : tables
4362
+ tables: group ? json[0] ?? null : json
4206
4363
  });
4207
4364
  return;
4208
4365
  }
4209
4366
  const c = painterFor(cfg);
4210
- for (const g of wanted) {
4211
- const rows = computeStandings(fixturesByGroup(g, matches));
4212
- if (rows.length === 0) {
4213
- out();
4214
- out(c.dim(" " + t("table.none", { group: g })));
4215
- continue;
4216
- }
4367
+ if (tables.length === 0) {
4368
+ out();
4369
+ out(
4370
+ c.dim(
4371
+ " " + (group ? t("table.none", { group: group.toUpperCase() }) : t("table.empty"))
4372
+ )
4373
+ );
4374
+ out();
4375
+ if (degraded) out(c.dim(" " + t("table.degraded")));
4376
+ out(disclaimer(t, c));
4377
+ return;
4378
+ }
4379
+ for (const { group: g, rows } of tables) {
4217
4380
  out();
4218
4381
  out(header(t("table.title", { group: g }), c));
4219
4382
  const table = new Table({
@@ -4243,6 +4406,7 @@ async function cmdTable(group, ctx) {
4243
4406
  out(table.toString());
4244
4407
  }
4245
4408
  out();
4409
+ if (degraded) out(c.dim(" " + t("table.degraded")));
4246
4410
  const src = dataSource(source, c);
4247
4411
  if (src) out(src);
4248
4412
  out(disclaimer(t, c));
@@ -4298,20 +4462,15 @@ function cmdInitHook(opts, { cfg }) {
4298
4462
  async function cmdMatch(id, ctx) {
4299
4463
  const { cfg, t } = ctx;
4300
4464
  precheck(cfg, t);
4301
- const adapter = adapterFor(ctx);
4302
- let match = allFixtures().find((m) => m.id === id);
4303
- let liveSource;
4304
- try {
4305
- if (match) {
4306
- const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
4307
- match = live.find((m) => m.id === id) ?? match;
4308
- liveSource = adapter.name;
4309
- }
4310
- } catch {
4311
- }
4465
+ const { match, degraded, source: liveSource } = await getMatchById(adapterFor(ctx), id);
4312
4466
  const marketSignal = match ? await reliableMarketSignalFor(ctx, match) : void 0;
4313
4467
  if (cfg.json) {
4314
- emitJson({ match: match ?? null, source: liveSource ?? null, marketSignal: marketSignal ?? null });
4468
+ emitJson({
4469
+ degraded,
4470
+ match: match ?? null,
4471
+ source: liveSource ?? null,
4472
+ marketSignal: marketSignal ?? null
4473
+ });
4315
4474
  return;
4316
4475
  }
4317
4476
  const c = painterFor(cfg);
@@ -4351,8 +4510,13 @@ var MARKET_INFO = "Prediction-market data is informational only.";
4351
4510
  function marketDisplayable(sig) {
4352
4511
  return !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
4353
4512
  }
4354
- function marketHeaderLine(m) {
4355
- return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag}`;
4513
+ function marketHeaderLine(m, cfg) {
4514
+ const when = formatDate(m.kickoff, { tz: cfg.tz, locale: cfg.lang });
4515
+ return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag} \xB7 ${when}`;
4516
+ }
4517
+ function noSignalLine(m, now) {
4518
+ if (marketRelevant(m, now)) return "No market signal for this match.";
4519
+ return isFinished(m.status) ? "Match has finished \u2014 market signals are pre-match and in-play reads." : "Match appears to have finished \u2014 market signals are pre-match and in-play reads.";
4356
4520
  }
4357
4521
  function printMarketBlock(m, sig, c) {
4358
4522
  for (const line2 of marketBlock(sig, m)) out(" " + c.dim(line2));
@@ -4361,10 +4525,10 @@ async function cmdMarkets(target, team, ctx) {
4361
4525
  const { cfg, t } = ctx;
4362
4526
  if (target === "next") {
4363
4527
  precheck(cfg, t);
4364
- if (!team) throw new InputError("Usage: claudinho markets next <team>");
4365
- const code = team.toUpperCase();
4366
- const fixture = nextFixtureForTeam(code);
4367
- const sig = fixture ? (await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS)).get(fixture.id) : void 0;
4528
+ const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)");
4529
+ const now2 = ctx.now ?? /* @__PURE__ */ new Date();
4530
+ const { match: fixture } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
4531
+ const sig = fixture && marketRelevant(fixture, now2) ? (await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS)).get(fixture.id) : void 0;
4368
4532
  const shown = sig && marketDisplayable(sig) ? sig : void 0;
4369
4533
  if (cfg.json) {
4370
4534
  emitJson({
@@ -4380,10 +4544,10 @@ async function cmdMarkets(target, team, ctx) {
4380
4544
  if (!fixture) {
4381
4545
  out(c2.dim(" " + t("next.none", { team: code })));
4382
4546
  } else {
4383
- out(header(marketHeaderLine(fixture), c2));
4547
+ out(header(marketHeaderLine(fixture, cfg), c2));
4384
4548
  out();
4385
4549
  if (shown) printMarketBlock(fixture, shown, c2);
4386
- else out(c2.dim(" No market signal for this match."));
4550
+ else out(c2.dim(" " + noSignalLine(fixture, now2)));
4387
4551
  }
4388
4552
  out();
4389
4553
  out(disclaimer(t, c2));
@@ -4392,8 +4556,9 @@ async function cmdMarkets(target, team, ctx) {
4392
4556
  }
4393
4557
  if (target && target !== "today" && !isValidDate(target)) {
4394
4558
  precheck(cfg, t);
4395
- const match = allFixtures().find((m) => m.id === target);
4396
- const sig = match ? (await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS)).get(match.id) : void 0;
4559
+ const now2 = ctx.now ?? /* @__PURE__ */ new Date();
4560
+ const { match } = await getMatchById(adapterFor(ctx), target);
4561
+ const sig = match && marketRelevant(match, now2) ? (await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS)).get(match.id) : void 0;
4397
4562
  const shown = sig && marketDisplayable(sig) ? sig : void 0;
4398
4563
  if (cfg.json) {
4399
4564
  emitJson({ matchId: target, informationalOnly: true, signal: shown ?? null });
@@ -4404,10 +4569,10 @@ async function cmdMarkets(target, team, ctx) {
4404
4569
  if (!match) {
4405
4570
  out(c2.dim(" " + t("match.none", { id: target })));
4406
4571
  } else {
4407
- out(header(marketHeaderLine(match), c2));
4572
+ out(header(marketHeaderLine(match, cfg), c2));
4408
4573
  out();
4409
4574
  if (shown) printMarketBlock(match, shown, c2);
4410
- else out(c2.dim(" No market signal for this match."));
4575
+ else out(c2.dim(" " + noSignalLine(match, now2)));
4411
4576
  }
4412
4577
  out();
4413
4578
  out(disclaimer(t, c2));
@@ -4416,11 +4581,13 @@ async function cmdMarkets(target, team, ctx) {
4416
4581
  }
4417
4582
  const explicitDate = target && target !== "today" ? target : void 0;
4418
4583
  precheck(cfg, t, explicitDate);
4419
- const date = explicitDate ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
4584
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
4585
+ const date = explicitDate ?? localDate(now.toISOString(), cfg.tz);
4420
4586
  const { matches } = await getMatchesForDate(adapterFor(ctx), date);
4421
4587
  const todays = fixturesByDate(date, matches, cfg.tz);
4422
- const signals = await marketSignalsFor(ctx, todays, MARKETS_CMD_OPTS);
4423
- const rows = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
4588
+ const relevant = todays.filter((m) => marketRelevant(m, now));
4589
+ const signals = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
4590
+ const rows = relevant.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
4424
4591
  (r) => !!r.signal && marketDisplayable(r.signal)
4425
4592
  );
4426
4593
  if (cfg.json) {
@@ -4437,7 +4604,7 @@ async function cmdMarkets(target, team, ctx) {
4437
4604
  out(c.dim(` No market signals available for ${date}.`));
4438
4605
  } else {
4439
4606
  for (const { match, signal } of rows) {
4440
- out(" " + c.bold(marketHeaderLine(match)));
4607
+ out(" " + c.bold(marketHeaderLine(match, cfg)));
4441
4608
  printMarketBlock(match, signal, c);
4442
4609
  out();
4443
4610
  }
@@ -4478,6 +4645,38 @@ function emitShare(ctx, e, copy) {
4478
4645
  );
4479
4646
  }
4480
4647
  }
4648
+ function emitShareTable(ctx, e, copy) {
4649
+ const snippet = formatShareTable(
4650
+ {
4651
+ tables: e.tables,
4652
+ source: e.source,
4653
+ installLine: e.installLine,
4654
+ emptyNote: e.emptyNote,
4655
+ degraded: e.degraded
4656
+ },
4657
+ e.options
4658
+ );
4659
+ if (ctx.cfg.json) {
4660
+ emitJson({
4661
+ kind: "table",
4662
+ target: "table",
4663
+ ...e.group ? { group: e.group } : {},
4664
+ source: e.source ?? null,
4665
+ degraded: e.degraded,
4666
+ informationalOnly: true,
4667
+ snippet,
4668
+ tables: e.tables.map((tb) => ({ group: tb.group, standings: tb.rows }))
4669
+ });
4670
+ } else {
4671
+ out(snippet);
4672
+ }
4673
+ if (copy) {
4674
+ const ok = (ctx.copy ?? copyToClipboard)(snippet);
4675
+ process.stderr.write(
4676
+ (ok ? "Copied share snippet to clipboard." : "Clipboard unavailable; printed snippet instead.") + "\n"
4677
+ );
4678
+ }
4679
+ }
4481
4680
  async function cmdShare(target, team, opts, ctx) {
4482
4681
  const { cfg, t } = ctx;
4483
4682
  const baseOptions = {
@@ -4511,11 +4710,30 @@ async function cmdShare(target, team, opts, ctx) {
4511
4710
  );
4512
4711
  return;
4513
4712
  }
4713
+ if (target === "table") {
4714
+ precheck(cfg, t);
4715
+ const group = team?.toUpperCase();
4716
+ const { tables, degraded, source: source2 } = await getStandings(adapterFor(ctx), group);
4717
+ emitShareTable(
4718
+ ctx,
4719
+ {
4720
+ group,
4721
+ tables,
4722
+ // Degraded ⇒ a static roster, served by no live provider: no attribution.
4723
+ source: degraded ? void 0 : source2,
4724
+ degraded,
4725
+ installLine: group ? `npx @claudinho/cli table ${group}` : "npx @claudinho/cli table",
4726
+ emptyNote: group ? `No group ${group}.` : "No standings available.",
4727
+ options: baseOptions
4728
+ },
4729
+ copy
4730
+ );
4731
+ return;
4732
+ }
4514
4733
  if (target === "next") {
4515
4734
  precheck(cfg, t);
4516
- if (!team) throw new InputError("Usage: claudinho share next <team>");
4517
- const code = team.toUpperCase();
4518
- const fixture = nextFixtureForTeam(code);
4735
+ const code = resolveTeamArg(team, "Usage: claudinho share next <team> (or set CLAUDINHO_TEAM)");
4736
+ const fixture = nextFixtureForTeam(code, { from: ctx.now ?? /* @__PURE__ */ new Date() });
4519
4737
  const matches = fixture ? [fixture] : [];
4520
4738
  const signals2 = await reliableShareSignals(ctx, matches);
4521
4739
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
@@ -4542,17 +4760,7 @@ async function cmdShare(target, team, opts, ctx) {
4542
4760
  }
4543
4761
  if (target && target !== "today" && !isValidDate(target)) {
4544
4762
  precheck(cfg, t);
4545
- const adapter = adapterFor(ctx);
4546
- let match = allFixtures().find((m) => m.id === target);
4547
- let source2;
4548
- try {
4549
- if (match) {
4550
- const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
4551
- match = live.find((m) => m.id === target) ?? match;
4552
- source2 = adapter.name;
4553
- }
4554
- } catch {
4555
- }
4763
+ const { match, source: source2 } = await getMatchById(adapterFor(ctx), target);
4556
4764
  const matches = match ? [match] : [];
4557
4765
  const signals2 = await reliableShareSignals(ctx, matches);
4558
4766
  emitShare(
@@ -4614,16 +4822,53 @@ var VIBES = [
4614
4822
  "Stoppage time and a clean stack trace.",
4615
4823
  "Coding into extra time."
4616
4824
  ];
4825
+ var VIBES_OPENER = [
4826
+ "Day one of the tournament. Save your work \u2014 it\u2019s about to get loud.",
4827
+ "Opening day: fresh bracket, fresh branch."
4828
+ ];
4829
+ var VIBES_FINAL = [
4830
+ "Final day. One last build, one last whistle.",
4831
+ "Ship it before the trophy does."
4832
+ ];
4833
+ function vibeLiveSegment(live, team) {
4834
+ const code = team?.toUpperCase();
4835
+ const pick2 = (code && live.find((m) => m.home.code === code || m.away.code === code)) ?? live[0];
4836
+ if (!pick2) return void 0;
4837
+ const minute = pick2.status === "HT" ? "HT" : pick2.minute ? `${pick2.minute}'` : "LIVE";
4838
+ return `${pick2.home.flag} ${scoreline(pick2)} ${pick2.away.flag} ${minute}`;
4839
+ }
4840
+ function vibePool(todayLocal, fixtures = allFixtures()) {
4841
+ let first;
4842
+ let last;
4843
+ for (const m of fixtures) {
4844
+ const d = m.kickoff.slice(0, 10);
4845
+ if (!first || d < first) first = d;
4846
+ if (!last || d > last) last = d;
4847
+ }
4848
+ if (todayLocal === first) return [...VIBES, ...VIBES_OPENER];
4849
+ if (todayLocal === last) return [...VIBES, ...VIBES_FINAL];
4850
+ return VIBES;
4851
+ }
4617
4852
  function cmdVibe(ctx) {
4618
4853
  const { cfg } = ctx;
4619
- const line2 = VIBES[Math.floor(Math.random() * VIBES.length)];
4854
+ const pool = vibePool(localDate((ctx.now ?? /* @__PURE__ */ new Date()).toISOString(), cfg.tz));
4855
+ const line2 = pool[Math.floor(Math.random() * pool.length)];
4856
+ let liveSeg;
4857
+ try {
4858
+ const state = readCurrentState(cfg.source, resolveCompetition());
4859
+ liveSeg = vibeLiveSegment(
4860
+ liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()),
4861
+ process.env.CLAUDINHO_TEAM
4862
+ );
4863
+ } catch {
4864
+ }
4620
4865
  if (cfg.json) {
4621
- emitJson({ vibe: line2, tag: "#VibingLaVidaLoca" });
4866
+ emitJson({ vibe: line2, tag: "#VibingLaVidaLoca", ...liveSeg ? { live: liveSeg } : {} });
4622
4867
  return;
4623
4868
  }
4624
4869
  const c = painterFor(cfg);
4625
4870
  out();
4626
- out(" \u26BD " + c.bold(line2));
4871
+ out(" \u26BD " + (liveSeg ? `${liveSeg} \u2014 ` : "") + c.bold(line2 ?? ""));
4627
4872
  out(" " + c.cyan("#VibingLaVidaLoca"));
4628
4873
  out();
4629
4874
  }
@@ -4637,7 +4882,7 @@ function handlePipeError(stream) {
4637
4882
  }
4638
4883
  handlePipeError(process.stdout);
4639
4884
  handlePipeError(process.stderr);
4640
- var VERSION = "0.4.2";
4885
+ var VERSION = "0.5.0";
4641
4886
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
4642
4887
  function ctxFrom(cmd) {
4643
4888
  const root = cmd.parent ?? cmd;
@@ -4652,7 +4897,9 @@ function fail(err) {
4652
4897
  process.exit(1);
4653
4898
  }
4654
4899
  var program = new Command();
4655
- program.name("claudinho").description("The 2026 football tournament in your terminal.\n" + DISCLAIMER).version(VERSION, "-v, --version").option("--lang <code>", "language: en, es, pt, fr").option("--tz <zone>", "IANA timezone, e.g. America/Mexico_City").option("--json", "output JSON (for scripting)").option("--no-color", "disable ANSI colors").option("--source <name>", "live data provider (advanced)").option("--flavor <level>", "commentary flair: off, subtle, full (default: full)").option("--no-markets", "hide prediction-market signals (informational odds)");
4900
+ program.name("claudinho").description(
4901
+ "The 2026 men\u2019s football tournament in your terminal, your Claude Code statusline, and any MCP client.\n" + DISCLAIMER
4902
+ ).version(VERSION, "-v, --version").option("--lang <code>", "language: en, es, pt, fr").option("--tz <zone>", "IANA timezone, e.g. America/Mexico_City").option("--json", "output JSON (for scripting)").option("--no-color", "disable ANSI colors").option("--source <name>", "live data provider (advanced)").option("--flavor <level>", "commentary flair: off, subtle, full (default: full)").option("--no-markets", "hide prediction-market signals (informational only)");
4656
4903
  program.addHelpText("after", "\n#VibingLaVidaLoca \u26BD");
4657
4904
  program.command("today").description("show a day's fixtures (default: today)").argument("[date]", "date as YYYY-MM-DD").action(async (date, _opts, cmd) => {
4658
4905
  try {
@@ -4668,7 +4915,7 @@ program.command("live").description("show matches in play right now").action(asy
4668
4915
  fail(e);
4669
4916
  }
4670
4917
  });
4671
- program.command("next").description("show a team's next fixture").argument("<team>", "team code, e.g. MEX").action(async (team, _opts, cmd) => {
4918
+ 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) => {
4672
4919
  try {
4673
4920
  await cmdNext(team, ctxFrom(cmd));
4674
4921
  } catch (e) {
@@ -4689,14 +4936,17 @@ program.command("match").description("show a single match by id").argument("<id>
4689
4936
  fail(e);
4690
4937
  }
4691
4938
  });
4692
- 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" (e.g. MEX)').action(async (target, team, _opts, cmd) => {
4939
+ 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) => {
4693
4940
  try {
4694
4941
  await cmdMarkets(target, team, ctxFrom(cmd));
4695
4942
  } catch (e) {
4696
4943
  fail(e);
4697
4944
  }
4698
4945
  });
4699
- program.command("share").description("print a shareable, copy-pasteable match snippet (#VibingLaVidaLoca)").argument("[target]", '"today" (default), "live", a date, a match id, or "next"').argument("[team]", 'team code when target is "next" (e.g. MEX)').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) => {
4946
+ program.command("share").description("print a shareable, copy-pasteable match snippet (#VibingLaVidaLoca)").argument("[target]", '"today" (default), "live", a date, a match id, "next", or "table"').argument(
4947
+ "[team]",
4948
+ 'team code for "next" (default: $CLAUDINHO_TEAM), or group letter for "table" (omit for all groups)'
4949
+ ).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) => {
4700
4950
  try {
4701
4951
  await cmdShare(target, team, opts, ctxFrom(cmd));
4702
4952
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.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 and Claude Code statusline. No API keys. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,7 +56,7 @@
56
56
  "tsup": "^8.0.0",
57
57
  "typescript": "^5.7.0",
58
58
  "vitest": "^4.1.0",
59
- "@claudinho/core": "0.4.2"
59
+ "@claudinho/core": "0.5.0"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsup",