@claudinho/cli 0.11.2 → 0.12.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 +324 -267
  2. package/dist/index.js +779 -159
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -335,6 +335,13 @@ var EN = {
335
335
  "status.fullTime": "full-time",
336
336
  "status.postponed": "postponed",
337
337
  "status.cancelled": "cancelled",
338
+ // A timeline row's kind word (core `timelineRows`).
339
+ "event.goal": "Goal",
340
+ "event.ownGoal": "Own goal",
341
+ "event.penalty": "Penalty",
342
+ "event.yellow": "Yellow card",
343
+ "event.red": "Red card",
344
+ "event.sub": "Substitution",
338
345
  "live.title": "Live now",
339
346
  "today.title": "Today's matches",
340
347
  "today.on": "Matches",
@@ -343,6 +350,23 @@ var EN = {
343
350
  "next.in": "in {countdown}",
344
351
  // A kickoff that has passed, where the countdown would say "in" (core `countdownPhrase`).
345
352
  "countdown.now": "now",
353
+ // The ambient line's syncing phrase (`claudinho ambient --json` alone; the
354
+ // statusline's own `prompt` stays English).
355
+ "ambient.syncing": "live \xB7 syncing\u2026",
356
+ // The plugin's moment words, sent by `claudinho ambient --json` as `words`
357
+ // (`{team}` a template the plugin fills with the record's own names). Each
358
+ // at most 40 characters; `{team}` once in `moment.goalFor` and `moment.win`
359
+ // and nowhere else, or the plugin falls back to its English words.
360
+ "moment.goal": "GOAL!",
361
+ "moment.ownGoal": "Own goal",
362
+ "moment.ownGoalDetail": "own goal",
363
+ "moment.goalFor": "Goal for {team}",
364
+ "moment.goals": "Goals",
365
+ "moment.scoreUpdated": "Score updated",
366
+ "moment.penalties": "Penalties",
367
+ "moment.fullTime": "Full time",
368
+ "moment.win": "{team} win",
369
+ "moment.draw": "Draw",
346
370
  // A title that introduces the lines below it (the MCP text).
347
371
  heading: "{title}:",
348
372
  // A list's empty state and the outage lines: the CLI's sentences, the MCP
@@ -354,6 +378,7 @@ var EN = {
354
378
  "feed.earlierRecord": "Live state could not be refreshed \u2014 showing the provider's earlier record.",
355
379
  "next.none": "No upcoming fixture found for {team}.",
356
380
  "match.none": "No match found with id {id}.",
381
+ "match.kickoff": "Kickoff",
357
382
  // The MCP text's own sentences for these states (the CLI says `live.degraded`
358
383
  // there): they keep the team or the id the answer is about.
359
384
  "next.unreachable": "Couldn't reach the data provider \u2014 no upcoming fixture confirmed for {team}.",
@@ -431,6 +456,12 @@ var ES = {
431
456
  "status.fullTime": "finalizado",
432
457
  "status.postponed": "aplazado",
433
458
  "status.cancelled": "cancelado",
459
+ "event.goal": "Gol",
460
+ "event.ownGoal": "Gol en contra",
461
+ "event.penalty": "Penal",
462
+ "event.yellow": "Tarjeta amarilla",
463
+ "event.red": "Tarjeta roja",
464
+ "event.sub": "Cambio",
434
465
  "live.title": "En vivo",
435
466
  "today.title": "Partidos de hoy",
436
467
  "today.on": "Partidos",
@@ -438,6 +469,17 @@ var ES = {
438
469
  "next.label": "Pr\xF3ximo partido de {team}",
439
470
  "next.in": "en {countdown}",
440
471
  "countdown.now": "ahora",
472
+ "ambient.syncing": "en vivo \xB7 sincronizando\u2026",
473
+ "moment.goal": "\xA1GOL!",
474
+ "moment.ownGoal": "Gol en contra",
475
+ "moment.ownGoalDetail": "gol en contra",
476
+ "moment.goalFor": "Gol de {team}",
477
+ "moment.goals": "Goles",
478
+ "moment.scoreUpdated": "Marcador corregido",
479
+ "moment.penalties": "Penales",
480
+ "moment.fullTime": "Final",
481
+ "moment.win": "Gana {team}",
482
+ "moment.draw": "Empate",
441
483
  heading: "{title}:",
442
484
  "today.none": "No hay partidos para esta fecha.",
443
485
  "live.none": "No hay partidos en juego ahora mismo.",
@@ -446,6 +488,7 @@ var ES = {
446
488
  "feed.earlierRecord": "No se pudo actualizar el estado en vivo \u2014 se muestra el registro anterior del proveedor.",
447
489
  "next.none": "No se encontr\xF3 pr\xF3ximo partido para {team}.",
448
490
  "match.none": "No se encontr\xF3 partido con id {id}.",
491
+ "match.kickoff": "Inicio",
449
492
  "next.unreachable": "No se pudo contactar al proveedor de datos \u2014 ning\xFAn pr\xF3ximo partido confirmado para {team}.",
450
493
  "match.unreachable": "No se pudo contactar al proveedor de datos \u2014 no se pudo consultar el partido {id}.",
451
494
  "team.ambiguous": '"{query}" es ambiguo. \xBFQuisiste decir:',
@@ -520,6 +563,12 @@ var PT = {
520
563
  "status.fullTime": "encerrado",
521
564
  "status.postponed": "adiado",
522
565
  "status.cancelled": "cancelado",
566
+ "event.goal": "Gol",
567
+ "event.ownGoal": "Gol contra",
568
+ "event.penalty": "P\xEAnalti",
569
+ "event.yellow": "Cart\xE3o amarelo",
570
+ "event.red": "Cart\xE3o vermelho",
571
+ "event.sub": "Substitui\xE7\xE3o",
523
572
  "live.title": "Ao vivo",
524
573
  "today.title": "Jogos de hoje",
525
574
  "today.on": "Jogos",
@@ -527,6 +576,17 @@ var PT = {
527
576
  "next.label": "Pr\xF3ximo jogo de {team}",
528
577
  "next.in": "em {countdown}",
529
578
  "countdown.now": "agora",
579
+ "ambient.syncing": "ao vivo \xB7 sincronizando\u2026",
580
+ "moment.goal": "GOL!",
581
+ "moment.ownGoal": "Gol contra",
582
+ "moment.ownGoalDetail": "gol contra",
583
+ "moment.goalFor": "Gol \xB7 {team}",
584
+ "moment.goals": "Gols",
585
+ "moment.scoreUpdated": "Placar corrigido",
586
+ "moment.penalties": "P\xEAnaltis",
587
+ "moment.fullTime": "Fim de jogo",
588
+ "moment.win": "{team} vence",
589
+ "moment.draw": "Empate",
530
590
  heading: "{title}:",
531
591
  "today.none": "Nenhum jogo para esta data.",
532
592
  "live.none": "Nenhum jogo em andamento agora.",
@@ -535,6 +595,7 @@ var PT = {
535
595
  "feed.earlierRecord": "N\xE3o foi poss\xEDvel atualizar o estado ao vivo \u2014 mostrando o registro anterior do provedor.",
536
596
  "next.none": "Nenhum pr\xF3ximo jogo encontrado para {team}.",
537
597
  "match.none": "Nenhum jogo encontrado com id {id}.",
598
+ "match.kickoff": "In\xEDcio",
538
599
  "next.unreachable": "N\xE3o foi poss\xEDvel contatar o provedor de dados \u2014 nenhum pr\xF3ximo jogo confirmado para {team}.",
539
600
  "match.unreachable": "N\xE3o foi poss\xEDvel contatar o provedor de dados \u2014 o jogo {id} n\xE3o p\xF4de ser consultado.",
540
601
  "team.ambiguous": '"{query}" \xE9 amb\xEDguo. Voc\xEA quis dizer:',
@@ -609,6 +670,12 @@ var FR = {
609
670
  "status.fullTime": "termin\xE9",
610
671
  "status.postponed": "report\xE9",
611
672
  "status.cancelled": "annul\xE9",
673
+ "event.goal": "But",
674
+ "event.ownGoal": "But contre son camp",
675
+ "event.penalty": "Penalty",
676
+ "event.yellow": "Carton jaune",
677
+ "event.red": "Carton rouge",
678
+ "event.sub": "Remplacement",
612
679
  "live.title": "En direct",
613
680
  "today.title": "Matchs d'aujourd'hui",
614
681
  "today.on": "Matchs",
@@ -616,6 +683,17 @@ var FR = {
616
683
  "next.label": "Prochain match de {team}",
617
684
  "next.in": "dans {countdown}",
618
685
  "countdown.now": "maintenant",
686
+ "ambient.syncing": "en direct \xB7 synchronisation\u2026",
687
+ "moment.goal": "BUT !",
688
+ "moment.ownGoal": "But contre son camp",
689
+ "moment.ownGoalDetail": "contre son camp",
690
+ "moment.goalFor": "But pour {team}",
691
+ "moment.goals": "Buts",
692
+ "moment.scoreUpdated": "Score corrig\xE9",
693
+ "moment.penalties": "Tirs au but",
694
+ "moment.fullTime": "Fin du match",
695
+ "moment.win": "{team} gagne",
696
+ "moment.draw": "Match nul",
619
697
  heading: "{title} :",
620
698
  "today.none": "Aucun match pr\xE9vu pour cette date.",
621
699
  "live.none": "Aucun match en cours pour l'instant.",
@@ -624,6 +702,7 @@ var FR = {
624
702
  "feed.earlierRecord": "L\u2019\xE9tat en direct n\u2019a pas pu \xEAtre actualis\xE9 \u2014 affichage de l\u2019enregistrement pr\xE9c\xE9dent du fournisseur.",
625
703
  "next.none": "Aucun prochain match trouv\xE9 pour {team}.",
626
704
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
705
+ "match.kickoff": "Coup d\u2019envoi",
627
706
  "next.unreachable": "Impossible de joindre le fournisseur de donn\xE9es \u2014 aucun prochain match confirm\xE9 pour {team}.",
628
707
  "match.unreachable": "Impossible de joindre le fournisseur de donn\xE9es \u2014 le match {id} n\u2019a pas pu \xEAtre consult\xE9.",
629
708
  "team.ambiguous": '"{query}" est ambigu. Vouliez-vous dire :',
@@ -657,6 +736,20 @@ function t(lang, key, vars) {
657
736
  (whole, name) => Object.hasOwn(vars, name) ? vars[name] : whole
658
737
  );
659
738
  }
739
+ function momentWords(lang) {
740
+ return {
741
+ goal: t(lang, "moment.goal"),
742
+ ownGoal: t(lang, "moment.ownGoal"),
743
+ ownGoalDetail: t(lang, "moment.ownGoalDetail"),
744
+ goalFor: t(lang, "moment.goalFor"),
745
+ goals: t(lang, "moment.goals"),
746
+ scoreUpdated: t(lang, "moment.scoreUpdated"),
747
+ penalties: t(lang, "moment.penalties"),
748
+ fullTime: t(lang, "moment.fullTime"),
749
+ win: t(lang, "moment.win"),
750
+ draw: t(lang, "moment.draw")
751
+ };
752
+ }
660
753
  var STAGE_KEYS = Object.freeze({
661
754
  GROUP: "stage.groupStage",
662
755
  R32: "stage.r32",
@@ -838,6 +931,65 @@ function withFlag(text, flag2, side) {
838
931
  if (typeof flag2 !== "string" || flag2 === "") return text;
839
932
  return side === "home" ? `${flag2} ${text}` : `${text} ${flag2}`;
840
933
  }
934
+ var KIND_KEYS = Object.freeze({
935
+ GOAL: "event.goal",
936
+ OWN_GOAL: "event.ownGoal",
937
+ PEN: "event.penalty",
938
+ YELLOW: "event.yellow",
939
+ RED: "event.red",
940
+ SUB: "event.sub"
941
+ });
942
+ var KIND_MARKS = Object.freeze({
943
+ GOAL: "\u26BD",
944
+ OWN_GOAL: "\u26BD",
945
+ PEN: "\u26BD",
946
+ YELLOW: "\u{1F7E8}",
947
+ RED: "\u{1F7E5}",
948
+ SUB: "\u{1F504}"
949
+ });
950
+ function timelineParts(match, opts) {
951
+ const events = match.events ?? [];
952
+ if (events.length === 0) return [];
953
+ const cells = events.map((e) => {
954
+ const team = e.side === "home" ? match.home : match.away;
955
+ return {
956
+ minute: e.added !== void 0 ? `${e.minute}+${e.added}'` : `${e.minute}'`,
957
+ mark: KIND_MARKS[e.type],
958
+ kind: t(opts.lang, KIND_KEYS[e.type]),
959
+ label: withFlag(team.code, opts.flags ? team.flag : void 0, e.side),
960
+ player: e.player
961
+ };
962
+ });
963
+ const widest = (pick2) => Math.max(...cells.map((c) => displayWidth(pick2(c))));
964
+ const minuteW = widest((c) => c.minute);
965
+ const kindW = widest((c) => c.kind);
966
+ const labelW = widest((c) => c.label);
967
+ return cells.map((c) => {
968
+ const columns = ` ${padVisible(c.minute, minuteW)} ${c.mark} ${padVisible(c.kind, kindW)} ${padVisible(c.label, labelW)}`;
969
+ if (!c.player) return [{ text: columns.trimEnd() }];
970
+ return [{ text: `${columns} ` }, { text: c.player, style: "strong" }];
971
+ });
972
+ }
973
+ var SCORING = /* @__PURE__ */ new Set(["GOAL", "OWN_GOAL", "PEN"]);
974
+ function provenLastGoal(match) {
975
+ const score = match.score;
976
+ if (!score) return null;
977
+ let home = 0;
978
+ let away = 0;
979
+ let last = null;
980
+ const identities = /* @__PURE__ */ new Set();
981
+ for (const e of match.events ?? []) {
982
+ if (!SCORING.has(e.type)) continue;
983
+ const identity = JSON.stringify([e.type, e.minute, e.added ?? 0, e.side, e.player ?? null]);
984
+ if (identities.has(identity)) return null;
985
+ identities.add(identity);
986
+ if (e.side === "home") home++;
987
+ else if (e.side === "away") away++;
988
+ else continue;
989
+ last = e;
990
+ }
991
+ return last !== null && home === score.home && away === score.away ? last : null;
992
+ }
841
993
  function outcomeFromScore(home, away) {
842
994
  if (home > away) return "H";
843
995
  if (home < away) return "A";
@@ -1109,18 +1261,33 @@ function sealScorePair(raw) {
1109
1261
  const away = count(v.away, MAX_GOALS);
1110
1262
  return home !== void 0 && away !== void 0 ? { home, away } : void 0;
1111
1263
  }
1264
+ var MAX_ADDED_MINUTES = 30;
1265
+ var SIDES = /* @__PURE__ */ new Set(["home", "away"]);
1112
1266
  function sealEvent(raw) {
1113
- if (!raw || typeof raw !== "object") return void 0;
1267
+ if (!raw || typeof raw !== "object") return null;
1114
1268
  const e = raw;
1115
1269
  const type = member(e.type, EVENT_TYPES);
1116
- if (!type) return void 0;
1270
+ if (!type) return null;
1117
1271
  const minute = count(e.minute, MAX_MINUTE);
1118
- if (minute === void 0) return void 0;
1119
- const out2 = { type, minute, teamCode: humanLabel(e.teamCode, TEAM_CODE_COLUMNS) };
1272
+ if (minute === void 0) return null;
1273
+ let added;
1274
+ if (e.added !== void 0) {
1275
+ added = count(e.added, MAX_ADDED_MINUTES);
1276
+ if (added === void 0 || added < 1) return null;
1277
+ }
1278
+ const side = member(e.side, SIDES);
1279
+ if (!side) return null;
1280
+ const out2 = { type, minute };
1281
+ if (added !== void 0) out2.added = added;
1282
+ out2.side = side;
1283
+ out2.teamCode = humanLabel(e.teamCode, TEAM_CODE_COLUMNS);
1120
1284
  const player = humanLabel(e.player);
1121
1285
  if (player) out2.player = player;
1122
1286
  return out2;
1123
1287
  }
1288
+ function byClock(a, b) {
1289
+ return a.minute - b.minute || (a.added ?? 0) - (b.added ?? 0);
1290
+ }
1124
1291
  function sealMatch(parts, opts = {}) {
1125
1292
  if (!parts || typeof parts !== "object") return malformed("match is not an object");
1126
1293
  const id = opaqueId(parts.id, ESPN_ID);
@@ -1165,7 +1332,8 @@ function sealMatch(parts, opts = {}) {
1165
1332
  winnerCode = claimedWinner;
1166
1333
  }
1167
1334
  }
1168
- const events = opts.events === false ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e);
1335
+ const timelined = status === "LIVE" || status === "HT" || status === "FT";
1336
+ const events = opts.events === false || !timelined ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e).sort(byClock);
1169
1337
  const out2 = { id, stage };
1170
1338
  if (stageLabel2) out2.stageLabel = stageLabel2;
1171
1339
  if (group) out2.group = group;
@@ -4307,6 +4475,75 @@ function parseMinute(st) {
4307
4475
  }
4308
4476
  return void 0;
4309
4477
  }
4478
+ var DETAIL_FLAGS = ["scoringPlay", "yellowCard", "redCard", "penaltyKick", "ownGoal", "shootout"];
4479
+ var DETAIL_MINUTE = /^(\d{1,3})'$/;
4480
+ var DETAIL_MINUTE_ADDED = /^(\d{1,3})'\+(\d{1,2})'$/;
4481
+ function detailKind(d) {
4482
+ const f = {};
4483
+ for (const name of DETAIL_FLAGS) {
4484
+ const v = flag(d[name]);
4485
+ if (v === void 0) return void 0;
4486
+ f[name] = v;
4487
+ }
4488
+ if (f.shootout) return void 0;
4489
+ const { scoringPlay: s, yellowCard: y, redCard: r, penaltyKick: p, ownGoal: o } = f;
4490
+ if (s && !y && !r) {
4491
+ if (!p && !o) return "GOAL";
4492
+ if (o && !p) return "OWN_GOAL";
4493
+ if (p && !o) return "PEN";
4494
+ return void 0;
4495
+ }
4496
+ if (p || o || s) return void 0;
4497
+ if (y && !r) return "YELLOW";
4498
+ if (r && !y) return "RED";
4499
+ return void 0;
4500
+ }
4501
+ function detailMinute(clock) {
4502
+ if (!clock || typeof clock !== "object") return void 0;
4503
+ const dv = clock.displayValue;
4504
+ if (typeof dv !== "string") return void 0;
4505
+ const plain2 = DETAIL_MINUTE.exec(dv);
4506
+ if (plain2) return { minute: Number(plain2[1]) };
4507
+ const extra = DETAIL_MINUTE_ADDED.exec(dv);
4508
+ if (!extra) return void 0;
4509
+ return { minute: Number(extra[1]), added: Number(extra[2]) };
4510
+ }
4511
+ function rawTeamId(holder) {
4512
+ if (!holder || typeof holder !== "object") return void 0;
4513
+ const team = holder.team;
4514
+ if (!team || typeof team !== "object") return void 0;
4515
+ return opaqueId(team.id, ESPN_ID);
4516
+ }
4517
+ function detailEvents(details, sides) {
4518
+ const out2 = [];
4519
+ for (const raw of takeBounded(details, MAX_MATCH_EVENTS)) {
4520
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
4521
+ const d = raw;
4522
+ const type = detailKind(d);
4523
+ if (!type) continue;
4524
+ const when = detailMinute(d.clock);
4525
+ if (!when) continue;
4526
+ const teamId = rawTeamId(d);
4527
+ const side = teamId === void 0 ? void 0 : teamId === sides.homeId ? "home" : teamId === sides.awayId ? "away" : void 0;
4528
+ if (!side) continue;
4529
+ const event = { type, minute: when.minute };
4530
+ if (when.added !== void 0) event.added = when.added;
4531
+ event.side = side;
4532
+ event.teamCode = side === "home" ? sides.homeCode : sides.awayCode;
4533
+ const player = detailPlayer(d.athletesInvolved, type === "OWN_GOAL" ? side === "home" ? sides.awayId : sides.homeId : teamId);
4534
+ if (player) event.player = player;
4535
+ out2.push(event);
4536
+ }
4537
+ return out2;
4538
+ }
4539
+ function detailPlayer(athletes, expectedTeamId) {
4540
+ if (!Array.isArray(athletes) || athletes.length !== 1 || expectedTeamId === void 0) return void 0;
4541
+ const a = athletes[0];
4542
+ if (rawTeamId(a) !== expectedTeamId) return void 0;
4543
+ const name = a.displayName;
4544
+ if (!isHumanLabel(name)) return void 0;
4545
+ return humanLabel(name) || void 0;
4546
+ }
4310
4547
  var MAX_SLUG_UNITS2 = 64;
4311
4548
  var SLUG = /^[a-z0-9-]+$/;
4312
4549
  var EXACT_STAGE = Object.freeze({
@@ -4428,7 +4665,14 @@ function parseEspnEvent(raw, ctx = {}) {
4428
4665
  status,
4429
4666
  winnerCode,
4430
4667
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4431
- events: ev.events
4668
+ // The goals and cards: ESPN's `details`, read by the detail grammar; the
4669
+ // seal keeps them only on a status that holds a score.
4670
+ events: detailEvents(comp?.details, {
4671
+ homeId: rawTeamId(homeRaw),
4672
+ awayId: rawTeamId(awayRaw),
4673
+ homeCode: home.code,
4674
+ awayCode: away.code
4675
+ })
4432
4676
  },
4433
4677
  // The final reseal states the same kind the competitors were built with.
4434
4678
  { teamKind: kind }
@@ -4745,7 +4989,7 @@ function fetchMeta(result) {
4745
4989
  }
4746
4990
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
4747
4991
  var DEFAULT_COMPETITION = BUNDLED_SLUG;
4748
- var USER_AGENT = `claudinho/${"0.11.2"} (+https://github.com/arturogarrido/claudinho)`;
4992
+ var USER_AGENT = `claudinho/${"0.12.0"} (+https://github.com/arturogarrido/claudinho)`;
4749
4993
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
4750
4994
  var DEFAULT_COOLDOWN_MS = 5 * 6e4;
4751
4995
  var MAX_COOLDOWN_MS = 15 * 6e4;
@@ -6647,10 +6891,19 @@ async function getScheduleAhead(adapter, now = /* @__PURE__ */ new Date()) {
6647
6891
  };
6648
6892
  }
6649
6893
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
6650
- const { complete: _complete, ...result } = await getLiveRead(adapter, now);
6894
+ const { complete: _complete, finished: _finished, finishedTruncated: _truncated, ...result } = await getLiveRead(adapter, now);
6651
6895
  return result;
6652
6896
  }
6653
- async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
6897
+ var MAX_LIVE_FINISHED = 64;
6898
+ function finishedOf(records, seen) {
6899
+ const wanted = new Set(takeBounded(seen, MAX_LIVE_FINISHED));
6900
+ const done = records.filter((m) => m.status === "FT" && m.score !== void 0);
6901
+ const first = done.filter((m) => wanted.has(m.id));
6902
+ const rest = done.filter((m) => !wanted.has(m.id));
6903
+ const all = [...first, ...rest];
6904
+ return { finished: all.slice(0, MAX_LIVE_FINISHED), truncated: all.length > MAX_LIVE_FINISHED };
6905
+ }
6906
+ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date(), seen = []) {
6654
6907
  try {
6655
6908
  const day = now.toISOString().slice(0, 10);
6656
6909
  const fetched = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1), { acrossSeasons: true }) : await adapter.fetchLive();
@@ -6658,6 +6911,7 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
6658
6911
  const season = meta?.season;
6659
6912
  const matches = fetched.filter((m) => isLive(m.status));
6660
6913
  const between = betweenEditionsOf(adapter, meta, fetched, providerDayOf(adapter, now));
6914
+ const done = finishedOf(fetched, seen);
6661
6915
  return {
6662
6916
  matches,
6663
6917
  degraded: false,
@@ -6669,10 +6923,12 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
6669
6923
  // boolean below is the refresher's, unchanged: false for that, for an
6670
6924
  // adapter that said nothing, and for a failed read alike.
6671
6925
  ...partialOfRead(meta),
6672
- complete: meta?.complete === true
6926
+ complete: meta?.complete === true,
6927
+ finished: done.finished,
6928
+ finishedTruncated: done.truncated
6673
6929
  };
6674
6930
  } catch {
6675
- return { matches: [], degraded: true, complete: false };
6931
+ return { matches: [], degraded: true, complete: false, finished: [], finishedTruncated: false };
6676
6932
  }
6677
6933
  }
6678
6934
  function norm2(s) {
@@ -6908,22 +7164,29 @@ function sealMarketSignal(raw, options = {}) {
6908
7164
  if (sourceMarketId) out2.sourceMarketId = sourceMarketId;
6909
7165
  out2.asOf = canonicalTimestamp(s.asOf) ?? "";
6910
7166
  out2.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
7167
+ const judgedAt = options.now ?? /* @__PURE__ */ new Date();
7168
+ const fetchedMs = Date.parse(out2.fetchedAt);
7169
+ if (out2.fetchedAt === "" || !Number.isFinite(fetchedMs)) {
7170
+ return malformed("signal carries no readable fetch time");
7171
+ }
7172
+ if (fetchedMs > judgedAt.getTime()) {
7173
+ return unresolved("signal was fetched after the clock it is judged by");
7174
+ }
6911
7175
  out2.outcomes = outcomes;
6912
7176
  const isAmbiguous = s.ambiguous !== false;
6913
7177
  const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
6914
7178
  if (favorite) out2.favorite = favorite;
6915
7179
  if (liquidity !== void 0) out2.liquidity = liquidity;
6916
7180
  if (volume24h !== void 0) out2.volume24h = volume24h;
6917
- out2.stale = s.stale !== false;
7181
+ out2.stale = isStaleSignal(out2, { now: judgedAt, maxAgeMs: options.maxAgeMs });
6918
7182
  out2.ambiguous = isAmbiguous || out2.source === "";
6919
- out2.stale = out2.stale || isStaleSignal(out2, { now: options.now, maxAgeMs: options.maxAgeMs });
6920
7183
  return valid(out2);
6921
7184
  }
6922
7185
  function parseCachedMarketSignal(raw, options = {}) {
6923
7186
  return sealMarketSignal(raw, options);
6924
7187
  }
6925
7188
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
6926
- function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
7189
+ function marketRelevant(match, now) {
6927
7190
  if (isLive(match.status)) return true;
6928
7191
  if (isFinished(match.status)) return false;
6929
7192
  const k = Date.parse(match.kickoff);
@@ -7012,12 +7275,13 @@ function buildMarketSignal(input) {
7012
7275
  const outcomes = normalizeOutcomes(input.outcomes);
7013
7276
  const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
7014
7277
  const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
7278
+ const clock = input.now ?? /* @__PURE__ */ new Date();
7015
7279
  const signal = {
7016
7280
  matchId: input.match.id,
7017
7281
  source: input.source,
7018
7282
  sourceMarketId: input.sourceMarketId,
7019
7283
  asOf: input.asOf,
7020
- fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7284
+ fetchedAt: input.fetchedAt ?? clock.toISOString(),
7021
7285
  outcomes,
7022
7286
  favorite,
7023
7287
  liquidity: input.liquidity,
@@ -7025,8 +7289,8 @@ function buildMarketSignal(input) {
7025
7289
  stale: false,
7026
7290
  ambiguous: ambiguous2
7027
7291
  };
7028
- signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
7029
- const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
7292
+ signal.stale = isStaleSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
7293
+ const sealed = sealMarketSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
7030
7294
  if (sealed.kind !== "valid") {
7031
7295
  return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
7032
7296
  }
@@ -8780,17 +9044,33 @@ function painterFor(cfg) {
8780
9044
  return paint(cfg.color);
8781
9045
  }
8782
9046
  function statusToken(m, t2, c) {
9047
+ const word = statusWord(m, t2);
9048
+ switch (m.status) {
9049
+ case "LIVE":
9050
+ return c.green(word);
9051
+ case "HT":
9052
+ return c.yellow(word);
9053
+ case "FT":
9054
+ return c.gray(word);
9055
+ case "POSTPONED":
9056
+ case "CANCELLED":
9057
+ return c.red(word);
9058
+ default:
9059
+ return "";
9060
+ }
9061
+ }
9062
+ function statusWord(m, t2) {
8783
9063
  switch (m.status) {
8784
9064
  case "LIVE":
8785
- return c.green(`${m.minute ? `${m.minute}'` : t2("status.live")}`);
9065
+ return m.minute ? `${m.minute}'` : t2("status.live");
8786
9066
  case "HT":
8787
- return c.yellow(t2("status.ht"));
9067
+ return t2("status.ht");
8788
9068
  case "FT":
8789
- return c.gray(t2("status.ft"));
9069
+ return t2("status.ft");
8790
9070
  case "POSTPONED":
8791
- return c.red(t2("status.postponed"));
9071
+ return t2("status.postponed");
8792
9072
  case "CANCELLED":
8793
- return c.red(t2("status.cancelled"));
9073
+ return t2("status.cancelled");
8794
9074
  default:
8795
9075
  return "";
8796
9076
  }
@@ -9013,12 +9293,60 @@ function writeState(state, now = stampOrNow(state.updatedAt)) {
9013
9293
  const until = envelopeDeadline(replaced, now);
9014
9294
  if (until !== void 0 && !writeBackoffNote(state.source, state.competition, until, now)) return false;
9015
9295
  }
9016
- writeFileAtomic(
9017
- cachePath(state.source, state.competition),
9018
- JSON.stringify({ ...state, version: CACHE_VERSION })
9019
- );
9296
+ writeFileAtomic(cachePath(state.source, state.competition), boundedSnapshot(withoutEvents(state)));
9020
9297
  return true;
9021
9298
  }
9299
+ function boundedSnapshot(state) {
9300
+ const serialize = (s) => JSON.stringify({ ...s, version: CACHE_VERSION });
9301
+ let text = serialize(state);
9302
+ if (Buffer.byteLength(text, "utf8") <= MAX_STATE_BYTES) return text;
9303
+ let next = state;
9304
+ const play = state.play;
9305
+ if (play && typeof play === "object" && !Array.isArray(play) && Array.isArray(play.finished)) {
9306
+ next = { ...next, play: { ...play, finished: [] } };
9307
+ text = serialize(next);
9308
+ if (Buffer.byteLength(text, "utf8") <= MAX_STATE_BYTES) return text;
9309
+ }
9310
+ if (Array.isArray(next.live)) {
9311
+ next = { ...next, live: next.live.map(dropLastGoal) };
9312
+ text = serialize(next);
9313
+ if (Buffer.byteLength(text, "utf8") <= MAX_STATE_BYTES) return text;
9314
+ }
9315
+ if ("play" in next) {
9316
+ const { play: _play, ...withoutPlay } = next;
9317
+ text = serialize(withoutPlay);
9318
+ }
9319
+ return text;
9320
+ }
9321
+ function dropLastGoal(m) {
9322
+ if (!m || typeof m !== "object" || !("lastGoal" in m)) return m;
9323
+ const copy = { ...m };
9324
+ delete copy.lastGoal;
9325
+ return copy;
9326
+ }
9327
+ function withoutEvents(state) {
9328
+ const strip = (list) => Array.isArray(list) ? list.map(dropEvents) : list;
9329
+ const out2 = { ...state, live: strip(state.live) };
9330
+ if (state.fixtures !== void 0) out2.fixtures = strip(state.fixtures);
9331
+ if (state.schedule?.fixtures !== void 0) out2.schedule = { ...state.schedule, fixtures: strip(state.schedule.fixtures) };
9332
+ const play = state.play;
9333
+ if (play && typeof play === "object" && !Array.isArray(play) && Array.isArray(play.finished)) {
9334
+ const p = play;
9335
+ out2.play = {
9336
+ ...p,
9337
+ finished: p.finished.map(
9338
+ (e) => e && typeof e === "object" && !Array.isArray(e) ? { ...e, match: dropEvents(e.match) } : e
9339
+ )
9340
+ };
9341
+ }
9342
+ return out2;
9343
+ }
9344
+ function dropEvents(m) {
9345
+ if (!m || typeof m !== "object" || !("events" in m)) return m;
9346
+ const copy = { ...m };
9347
+ delete copy.events;
9348
+ return copy;
9349
+ }
9022
9350
  function stampOrNow(stamp) {
9023
9351
  return validStamp(stamp) ? Date.parse(stamp) : Date.now();
9024
9352
  }
@@ -9242,7 +9570,7 @@ function isUsableSignal(s) {
9242
9570
  if (!s.favorite) return false;
9243
9571
  return hasSaneDistribution(s.outcomes);
9244
9572
  }
9245
- function readMarketCache(source, competition, now = Date.now()) {
9573
+ function readMarketCache(source, competition, now = Date.now(), requestNow) {
9246
9574
  const signals = /* @__PURE__ */ new Map();
9247
9575
  const checked = /* @__PURE__ */ new Set();
9248
9576
  const file = readFile();
@@ -9269,7 +9597,7 @@ function readMarketCache(source, competition, now = Date.now()) {
9269
9597
  checked.add(id);
9270
9598
  continue;
9271
9599
  }
9272
- const sealed = parseCachedMarketSignal(entry.signal, { now: new Date(now) });
9600
+ const sealed = parseCachedMarketSignal(entry.signal, { now: requestNow });
9273
9601
  if (sealed.kind !== "valid") continue;
9274
9602
  const clean = sealed.value;
9275
9603
  if (clean.matchId !== id || clean.source !== source) continue;
@@ -9367,6 +9695,104 @@ function copyToClipboard(text, platform = process.platform) {
9367
9695
  return false;
9368
9696
  }
9369
9697
 
9698
+ // src/play.ts
9699
+ var FINISHED_TTL_MS = 10 * 6e4;
9700
+ var SEEN_TTL_MS = 6 * 60 * 6e4;
9701
+ var MAX_PLAY = 64;
9702
+ var NO_READ = Object.freeze({ whole: false, matches: [], finished: [], finishedTruncated: false });
9703
+ var ID = /^[A-Za-z0-9._:-]{1,64}$/;
9704
+ function believedAt(at, now, ttl) {
9705
+ if (!validStamp(at)) return false;
9706
+ const ms = Date.parse(at);
9707
+ return ms <= now && now - ms < ttl;
9708
+ }
9709
+ function playRecordIds(play, now) {
9710
+ const ids = /* @__PURE__ */ new Set();
9711
+ if (!play || typeof play !== "object" || Array.isArray(play)) return ids;
9712
+ const p = play;
9713
+ for (const raw of takeBounded(p.seen, MAX_PLAY)) {
9714
+ if (!raw || typeof raw !== "object") continue;
9715
+ const { id, at } = raw;
9716
+ if (typeof id === "string" && ID.test(id) && believedAt(at, now, SEEN_TTL_MS)) ids.add(id);
9717
+ }
9718
+ for (const raw of takeBounded(p.finished, MAX_PLAY)) {
9719
+ if (!raw || typeof raw !== "object") continue;
9720
+ const { match, at } = raw;
9721
+ if (!match || typeof match !== "object" || !believedAt(at, now, FINISHED_TTL_MS)) continue;
9722
+ const id = match.id;
9723
+ if (typeof id === "string" && ID.test(id)) ids.add(id);
9724
+ }
9725
+ return ids;
9726
+ }
9727
+ function believedPlay(prev, now, kind) {
9728
+ const out2 = { seen: [], finished: [] };
9729
+ if (!prev || typeof prev !== "object" || Array.isArray(prev)) return out2;
9730
+ const p = prev;
9731
+ const ids = /* @__PURE__ */ new Set();
9732
+ for (const raw of takeBounded(p.seen, MAX_PLAY)) {
9733
+ if (!raw || typeof raw !== "object") continue;
9734
+ const { id, at } = raw;
9735
+ if (typeof id !== "string" || !ID.test(id) || ids.has(id) || !believedAt(at, now, SEEN_TTL_MS)) continue;
9736
+ ids.add(id);
9737
+ out2.seen.push({ id, at });
9738
+ }
9739
+ const done = /* @__PURE__ */ new Set();
9740
+ for (const raw of takeBounded(p.finished, MAX_PLAY)) {
9741
+ if (!raw || typeof raw !== "object") continue;
9742
+ const { match, at } = raw;
9743
+ if (!believedAt(at, now, FINISHED_TTL_MS)) continue;
9744
+ const sealed = parsedValue(parseCachedMatch(match, { events: false, teamKind: kind }));
9745
+ if (sealed?.status !== "FT" || !sealed.score || done.has(sealed.id)) continue;
9746
+ done.add(sealed.id);
9747
+ out2.finished.push({ match: sealed, at });
9748
+ }
9749
+ return out2;
9750
+ }
9751
+ function advancePlay(prev, read, now, kind = "club") {
9752
+ const nowMs = now.getTime();
9753
+ const believed = believedPlay(prev, nowMs, kind);
9754
+ if (!read.whole) return believed;
9755
+ const at = now.toISOString();
9756
+ const inPlay = /* @__PURE__ */ new Set();
9757
+ const seenNow = [];
9758
+ for (const m of read.matches) {
9759
+ if (typeof m?.id !== "string" || inPlay.has(m.id)) continue;
9760
+ inPlay.add(m.id);
9761
+ seenNow.push({ id: m.id, at });
9762
+ }
9763
+ const finishedRead = /* @__PURE__ */ new Map();
9764
+ for (const m of read.finished) {
9765
+ if (typeof m?.id === "string" && !finishedRead.has(m.id)) finishedRead.set(m.id, m);
9766
+ }
9767
+ const finished = [];
9768
+ const held = /* @__PURE__ */ new Set();
9769
+ for (const e of believed.finished) {
9770
+ const id = e.match.id;
9771
+ if (inPlay.has(id)) continue;
9772
+ const again = finishedRead.get(id);
9773
+ if (again) finished.push({ match: again, at: e.at });
9774
+ else if (read.finishedTruncated) finished.push(e);
9775
+ else continue;
9776
+ held.add(id);
9777
+ }
9778
+ const keptSeen = [];
9779
+ for (const e of believed.seen) {
9780
+ if (inPlay.has(e.id) || held.has(e.id)) continue;
9781
+ const done = finishedRead.get(e.id);
9782
+ if (done) {
9783
+ finished.push({ match: done, at });
9784
+ held.add(e.id);
9785
+ } else if (read.finishedTruncated) {
9786
+ keptSeen.push(e);
9787
+ }
9788
+ }
9789
+ const seen = [...keptSeen, ...seenNow];
9790
+ return {
9791
+ seen: seen.length > MAX_PLAY ? seen.slice(seen.length - MAX_PLAY) : seen,
9792
+ finished: finished.length > MAX_PLAY ? finished.slice(finished.length - MAX_PLAY) : finished
9793
+ };
9794
+ }
9795
+
9370
9796
  // src/scheduleSlice.ts
9371
9797
  var SCHEDULE_DISPLAY_MAX = 64;
9372
9798
  var RELEVANT_BACK_MS = 6 * 60 * 6e4;
@@ -9496,8 +9922,17 @@ function isMatchShaped(m) {
9496
9922
  const x = m;
9497
9923
  return !!x && typeof x === "object" && typeof x.id === "string" && typeof x.kickoff === "string" && !!x.home?.code && !!x.away?.code;
9498
9924
  }
9499
- function upcomingResolved(now, fixtures = allFixtures()) {
9500
- return [...fixtures].sort(byKickoff).filter((m) => isUpcoming(m, new Date(now)) && isResolvedFixture(m));
9925
+ var KICKOFF_DUE_MS = 30 * 6e4;
9926
+ function isDue(m, nowMs, played) {
9927
+ if (m.status !== "SCHEDULED" || played.has(m.id)) return false;
9928
+ const k = Date.parse(m.kickoff);
9929
+ return k <= nowMs && nowMs <= k + KICKOFF_DUE_MS;
9930
+ }
9931
+ function isAhead(m, nowMs) {
9932
+ return Date.parse(m.kickoff) > nowMs && isUpcoming(m, new Date(nowMs));
9933
+ }
9934
+ function countdownCandidates(nowMs, fixtures = allFixtures(), played) {
9935
+ return [...fixtures].sort(byKickoff).filter((m) => isResolvedFixture(m) && (isAhead(m, nowMs) || isDue(m, nowMs, played)));
9501
9936
  }
9502
9937
  function defaultTeamKind(defaultCompetition) {
9503
9938
  return defaultCompetition === false ? "club" : "nation";
@@ -9505,14 +9940,38 @@ function defaultTeamKind(defaultCompetition) {
9505
9940
  function teamTok(t2, flags) {
9506
9941
  return flags && t2.flag ? t2.flag : t2.code;
9507
9942
  }
9508
- function matchSegment(m, compact, flags) {
9509
- const minute = m.status === "HT" ? "HT" : m.minute ? `${m.minute}'` : "LIVE";
9510
- if (!flags) {
9511
- return `${m.home.code} ${scoreline(m)} ${m.away.code} ${minute}`;
9943
+ function matchSegment(m, compact, flags, lang) {
9944
+ const ht = lang === void 0 ? "HT" : t(lang, "status.ht");
9945
+ const live = lang === void 0 ? "LIVE" : t(lang, "status.live");
9946
+ const minute = m.status === "HT" ? ht : m.minute ? `${m.minute}'` : live;
9947
+ const home = !flags ? m.home.code : compact ? teamTok(m.home, true) : withFlag(m.home.code, m.home.flag, "home");
9948
+ const away = !flags ? m.away.code : compact ? teamTok(m.away, true) : withFlag(m.away.code, m.away.flag, "away");
9949
+ const score = scoreline(m);
9950
+ return { text: `${home} ${score} ${away} ${minute}`, score: [home.length + 1, home.length + 1 + score.length] };
9951
+ }
9952
+ var plain = (text) => ({ text, strong: [] });
9953
+ function sharedPrefix(a, b) {
9954
+ const n = Math.min(a.length, b.length);
9955
+ let i = 0;
9956
+ while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
9957
+ return i;
9958
+ }
9959
+ function keptRanges(from, to, ranges) {
9960
+ if (from === to) return [...ranges];
9961
+ const kept = sharedPrefix(from, to);
9962
+ return ranges.filter(([, end]) => end <= kept);
9963
+ }
9964
+ function partsOf(line, strong) {
9965
+ const parts = [];
9966
+ let at = 0;
9967
+ for (const [start, end] of strong) {
9968
+ if (start < at || end <= start || end > line.length) return [{ text: line }];
9969
+ if (start > at) parts.push({ text: line.slice(at, start) });
9970
+ parts.push({ text: line.slice(start, end), style: "strong" });
9971
+ at = end;
9512
9972
  }
9513
- const home = compact ? teamTok(m.home, true) : withFlag(m.home.code, m.home.flag, "home");
9514
- const away = compact ? teamTok(m.away, true) : withFlag(m.away.code, m.away.flag, "away");
9515
- return `${home} ${scoreline(m)} ${away} ${minute}`;
9973
+ if (at < line.length || parts.length === 0) parts.push({ text: line.slice(at) });
9974
+ return parts.map((p) => p.text).join("") === line ? parts : [{ text: line }];
9516
9975
  }
9517
9976
  var MAX_LIVE_CONSIDERED = 64;
9518
9977
  var MAX_LIVE_EXAMINED = 512;
@@ -9550,13 +10009,17 @@ function sealFixtures(raw, kind) {
9550
10009
  };
9551
10010
  }
9552
10011
  function liveMatchesFromCache(state, nowMs, kind) {
10012
+ return liveRecordsFromCache(state, nowMs, kind).list;
10013
+ }
10014
+ function liveRecordsFromCache(state, nowMs, kind) {
9553
10015
  const fresh = state && ageMs(state, nowMs) < DISPLAY_STALE_MS;
9554
10016
  const rawLive = fresh ? state?.live : [];
9555
10017
  if (!Array.isArray(rawLive)) {
9556
- return { items: [], total: 0, shown: 0, truncated: false, complete: false };
10018
+ return { list: { items: [], total: 0, shown: 0, truncated: false, complete: false }, sources: [] };
9557
10019
  }
9558
10020
  const liveArr = rawLive;
9559
10021
  const out2 = [];
10022
+ const sources = [];
9560
10023
  let inspected = 0;
9561
10024
  let readable = true;
9562
10025
  for (let i = 0; i < liveArr.length; i++) {
@@ -9573,19 +10036,24 @@ function liveMatchesFromCache(state, nowMs, kind) {
9573
10036
  continue;
9574
10037
  }
9575
10038
  const sealed = parsedValue(parseCachedMatch(m, { events: false, teamKind: kind }));
9576
- if (sealed) out2.push(sealed);
9577
- else readable = false;
10039
+ if (sealed) {
10040
+ out2.push(sealed);
10041
+ sources.push(raw);
10042
+ } else readable = false;
9578
10043
  }
9579
10044
  const exhausted = inspected === liveArr.length;
9580
10045
  const complete = exhausted && readable;
9581
10046
  return {
9582
- items: out2,
9583
- total: out2.length,
9584
- shown: out2.length,
9585
- truncated: !exhausted,
9586
- // False when we stopped early or a record was unreadable. In either case an
9587
- // empty list must not render as the authoritative "nothing is on".
9588
- complete
10047
+ list: {
10048
+ items: out2,
10049
+ total: out2.length,
10050
+ shown: out2.length,
10051
+ truncated: !exhausted,
10052
+ // False when we stopped early or a record was unreadable. In either case an
10053
+ // empty list must not render as the authoritative "nothing is on".
10054
+ complete
10055
+ },
10056
+ sources
9589
10057
  };
9590
10058
  }
9591
10059
  var DEFAULT_MAX_SEGMENTS = 8;
@@ -9593,11 +10061,13 @@ var MAX_LINE_COLUMNS = 200;
9593
10061
  function renderPrompt(state, opts = {}) {
9594
10062
  return promptLine(state, opts).line;
9595
10063
  }
9596
- function promptLine(state, opts = {}) {
9597
- const unfitted = renderPromptLine(state, opts);
9598
- return { line: truncateVisible(unfitted, lineColumns(opts)), unfitted };
10064
+ function promptLine(state, opts = {}, played) {
10065
+ const rendered = renderPromptLine(state, opts, played);
10066
+ const unfitted = rendered.text;
10067
+ const line = truncateVisible(unfitted, lineColumns(opts));
10068
+ return { line, unfitted, parts: partsOf(line, keptRanges(unfitted, line, rendered.strong)) };
9599
10069
  }
9600
- function renderPromptLine(state, opts = {}) {
10070
+ function renderPromptLine(state, opts = {}, played) {
9601
10071
  const now = opts.now ?? /* @__PURE__ */ new Date();
9602
10072
  const nowMs = now.getTime();
9603
10073
  const defaultCompetition = opts.defaultCompetition ?? true;
@@ -9615,23 +10085,27 @@ function renderPromptLine(state, opts = {}) {
9615
10085
  const max = picked ? 1 : opts.max && opts.max > 0 ? Math.min(opts.max, DEFAULT_MAX_SEGMENTS) : DEFAULT_MAX_SEGMENTS;
9616
10086
  const shown = ordered.slice(0, max);
9617
10087
  const overflow = live.length - shown.length;
9618
- return fitLiveLine(shown.map((m) => matchSegment(m, compact, flags)), overflow, liveList.complete, width);
10088
+ return fitLiveLine(shown.map((m) => matchSegment(m, compact, flags, opts.lang)), overflow, liveList.complete, width);
9619
10089
  }
9620
10090
  const win = syncingWindow(state, nowMs, liveWhole(liveList.complete, state), defaultCompetition, cachedFixtures, schedule, pick2);
9621
10091
  if (win) {
9622
10092
  const first = win[0];
9623
10093
  const matchup = first && isResolvedFixture(first) ? `${teamTok(first.home, flags)} vs ${teamTok(first.away, flags)} ` : "";
9624
10094
  const more = win.length - 1;
9625
- return fitWithMarker(`\u26BD ${matchup}live \xB7 syncing\u2026`, more > 0 ? ` +${more}` : "", width);
10095
+ const syncing = opts.lang === void 0 ? "live \xB7 syncing\u2026" : t(opts.lang, "ambient.syncing");
10096
+ return plain(fitWithMarker(`\u26BD ${matchup}${syncing}`, more > 0 ? ` +${more}` : "", width));
9626
10097
  }
9627
- const next = countdownFixture(nowMs, schedule, pick2);
10098
+ const next = countdownFixture(nowMs, schedule, pick2, played ?? playRecordIds(state?.play, nowMs));
9628
10099
  if (next) {
9629
- return `${teamTok(next.home, flags)} vs ${teamTok(next.away, flags)} in ${countdown(next.kickoff, now)}`;
10100
+ const matchup = `${teamTok(next.home, flags)} vs ${teamTok(next.away, flags)}`;
10101
+ if (opts.lang !== void 0) return plain(`\u26BD ${matchup} ${countdownPhrase(opts.lang, next.kickoff, now)}`);
10102
+ const left = countdown(next.kickoff, now);
10103
+ return plain(left === COUNTDOWN_NOW ? `\u26BD ${matchup} ${COUNTDOWN_NOW}` : `\u26BD ${matchup} in ${left}`);
9630
10104
  }
9631
10105
  if (defaultCompetition && isTournamentWindowOver(nowMs, schedule)) {
9632
- return TOURNAMENT_COMPLETE_LINE;
10106
+ return plain(TOURNAMENT_COMPLETE_LINE);
9633
10107
  }
9634
- return NOTHING_KNOWN_LINE;
10108
+ return plain(NOTHING_KNOWN_LINE);
9635
10109
  }
9636
10110
  function lineColumns(opts) {
9637
10111
  const c = opts.columns;
@@ -9645,10 +10119,18 @@ function fitWithMarker(body, marker, width) {
9645
10119
  }
9646
10120
  function fitLiveLine(segments, overflow, complete, width) {
9647
10121
  const markerFor = (dropped) => !complete ? " +more" : overflow + dropped > 0 ? ` +${overflow + dropped}` : "";
9648
- const bodyOf = (kept2) => "\u26BD " + segments.slice(0, kept2).join(" \xB7 ");
10122
+ const bodyOf = (kept2) => "\u26BD " + segments.slice(0, kept2).map((g) => g.text).join(" \xB7 ");
9649
10123
  let kept = segments.length;
9650
10124
  while (kept > 1 && displayWidth(bodyOf(kept)) + displayWidth(markerFor(segments.length - kept)) > width) kept--;
9651
- return fitWithMarker(bodyOf(kept), markerFor(segments.length - kept), width);
10125
+ const body = bodyOf(kept);
10126
+ const text = fitWithMarker(body, markerFor(segments.length - kept), width);
10127
+ const ranges = [];
10128
+ let offset = "\u26BD ".length;
10129
+ for (const g of segments.slice(0, kept)) {
10130
+ ranges.push([offset + g.score[0], offset + g.score[1]]);
10131
+ offset += g.text.length + " \xB7 ".length;
10132
+ }
10133
+ return { text, strong: keptRanges(body, text, ranges).filter(([, end]) => end <= text.length) };
9652
10134
  }
9653
10135
  function countdownSchedule(state, defaultCompetition, kind) {
9654
10136
  const cachedFixtureList = sealFixtures(defaultCompetition ? state?.fixtures : state?.schedule?.fixtures, kind);
@@ -9676,8 +10158,8 @@ function syncingWindow(state, nowMs, liveComplete, defaultCompetition, cachedFix
9676
10158
  const win = pickAmbientMatch(fixturesInLiveWindow(nowMs, schedule), pick2);
9677
10159
  return win.length > 0 ? win : void 0;
9678
10160
  }
9679
- function countdownFixture(nowMs, schedule, pick2) {
9680
- return pickAmbientMatch(upcomingResolved(nowMs, schedule), pick2)[0];
10161
+ function countdownFixture(nowMs, schedule, pick2, played) {
10162
+ return pickAmbientMatch(countdownCandidates(nowMs, schedule, played), pick2)[0];
9681
10163
  }
9682
10164
 
9683
10165
  // src/hook.ts
@@ -9695,9 +10177,12 @@ function rosterPinned(t2, pin) {
9695
10177
  const { team } = lookupTeam(t2.code);
9696
10178
  return team ? { ...t2, name: team.name, flag: team.flag } : t2;
9697
10179
  }
10180
+ function hookNames(m, pin) {
10181
+ return { home: rosterPinned(m.home, pin).name, away: rosterPinned(m.away, pin).name };
10182
+ }
9698
10183
  function hookLine(m, opts) {
9699
10184
  const { flags, pin } = opts;
9700
- const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
10185
+ const minute = m.status === "FT" ? "FT" : m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
9701
10186
  const h = rosterPinned(m.home, pin);
9702
10187
  const a = rosterPinned(m.away, pin);
9703
10188
  const home = flags ? withFlag(h.name, h.flag, "home") : h.name;
@@ -9722,36 +10207,85 @@ ${lines}`, more);
9722
10207
  }
9723
10208
 
9724
10209
  // src/ambient.ts
10210
+ function pinnedSideOf(m, pin) {
10211
+ if (pin === void 0) return null;
10212
+ if (isPinnedSide(m.home, pin)) return "home";
10213
+ return isPinnedSide(m.away, pin) ? "away" : null;
10214
+ }
10215
+ var SCORING2 = /* @__PURE__ */ new Set(["GOAL", "OWN_GOAL", "PEN"]);
10216
+ function lastGoalOf(m, raw) {
10217
+ if (raw === void 0 || raw === null) return null;
10218
+ const e = sealEvent(raw);
10219
+ if (!e || !SCORING2.has(e.type)) return null;
10220
+ if (e.teamCode !== (e.side === "home" ? m.home.code : m.away.code)) return null;
10221
+ return (m.score?.[e.side] ?? 0) >= 1 ? e : null;
10222
+ }
10223
+ function finishedFromCache(state, nowMs, kind, live) {
10224
+ if (state?.liveComplete !== true) return [];
10225
+ const play = state.play;
10226
+ if (!play || typeof play !== "object" || Array.isArray(play)) return [];
10227
+ const out2 = [];
10228
+ const ids = /* @__PURE__ */ new Set();
10229
+ for (const raw of takeBounded(play.finished, MAX_PLAY)) {
10230
+ if (!raw || typeof raw !== "object") continue;
10231
+ const { match, at } = raw;
10232
+ if (!validStamp(at)) continue;
10233
+ const atMs = Date.parse(at);
10234
+ if (atMs > nowMs || nowMs - atMs >= FINISHED_TTL_MS) continue;
10235
+ const sealed = parsedValue(parseCachedMatch(match, { events: false, teamKind: kind }));
10236
+ if (sealed?.status !== "FT" || !sealed.score || live.has(sealed.id) || ids.has(sealed.id)) continue;
10237
+ ids.add(sealed.id);
10238
+ out2.push(sealed);
10239
+ }
10240
+ return out2;
10241
+ }
9725
10242
  function ambientView(state, opts = {}) {
9726
10243
  const now = opts.now ?? /* @__PURE__ */ new Date();
9727
10244
  const nowMs = now.getTime();
9728
10245
  const defaultCompetition = opts.defaultCompetition ?? true;
9729
10246
  const kind = opts.teamKind ?? defaultTeamKind(opts.defaultCompetition);
9730
10247
  const pin = opts.pin ?? (opts.pick && "team" in opts.pick ? opts.pick.team : void 0);
9731
- const read = { ...opts, now };
10248
+ const { lang, ...rest } = opts;
10249
+ const read = { ...rest, now };
9732
10250
  const hookOpts = { flags: opts.flags ?? true, pin: defaultCompetition };
9733
- const list = liveMatchesFromCache(state, nowMs, kind);
10251
+ const { list, sources } = liveRecordsFromCache(state, nowMs, kind);
10252
+ const lastGoals = /* @__PURE__ */ new Map();
10253
+ list.items.forEach((m, i) => {
10254
+ lastGoals.set(m, lastGoalOf(m, sources[i]?.lastGoal));
10255
+ });
9734
10256
  const items = pickAmbientMatch(list.items, opts.pick).map((m) => ({
9735
10257
  ...m,
9736
10258
  picked: isPicked(m, opts.pick),
9737
10259
  pinned: pin !== void 0 && (isPinnedSide(m.home, pin) || isPinnedSide(m.away, pin)),
9738
- line: hookLine(m, hookOpts)
10260
+ pinnedSide: pinnedSideOf(m, pin),
10261
+ names: hookNames(m, defaultCompetition),
10262
+ line: hookLine(m, hookOpts),
10263
+ lastGoal: lastGoals.get(m) ?? null
9739
10264
  }));
9740
- const prompt = promptLine(state, read);
10265
+ const played = playRecordIds(state?.play, nowMs);
10266
+ const prompt = promptLine(state, { ...read, lang }, played);
9741
10267
  const context = renderHook(state, read);
9742
10268
  const { cachedFixtures, schedule } = countdownSchedule(state, defaultCompetition, kind);
9743
10269
  const age = stampAgeMs(state?.updatedAt, nowMs);
9744
10270
  const stamp = state && Number.isFinite(age) ? Date.parse(state.updatedAt) : void 0;
9745
10271
  const staleAt = stamp === void 0 ? void 0 : new Date(stamp + DISPLAY_STALE_MS);
10272
+ const finished = finishedFromCache(
10273
+ state,
10274
+ nowMs,
10275
+ kind,
10276
+ new Set(list.items.map((m) => m.id))
10277
+ ).map((m) => ({ ...m, pinnedSide: pinnedSideOf(m, pin), names: hookNames(m, defaultCompetition), line: hookLine(m, hookOpts) }));
9746
10278
  const syncing = list.items.length === 0 && syncingWindow(state, nowMs, liveWhole(list.complete, state), defaultCompetition, cachedFixtures, schedule, opts.pick) !== void 0;
9747
10279
  return {
9748
10280
  line: prompt.line,
10281
+ lineParts: prompt.parts,
9749
10282
  idle: prompt.unfitted === TOURNAMENT_COMPLETE_LINE,
9750
10283
  empty: prompt.unfitted === NOTHING_KNOWN_LINE,
9751
10284
  context: context === "" ? null : context,
9752
10285
  live: { items, total: list.total, shown: list.shown, truncated: list.truncated, complete: list.complete },
10286
+ finished,
9753
10287
  current: stamp !== void 0 && age < DISPLAY_STALE_MS && state?.degraded !== true && state?.liveComplete === true && !syncing,
9754
- next: countdownFixture(nowMs, schedule, opts.pick) ?? null,
10288
+ next: countdownFixture(nowMs, schedule, opts.pick, played) ?? null,
9755
10289
  degraded: state?.degraded === true,
9756
10290
  source: typeof state?.source === "string" ? state.source : null,
9757
10291
  updatedAt: stamp === void 0 ? null : new Date(stamp).toISOString(),
@@ -9783,6 +10317,16 @@ function inKnockoutPhase(nowMs, competition) {
9783
10317
  return !!next && isKnockoutStage(next.stage);
9784
10318
  }
9785
10319
  var NEVER = (/* @__PURE__ */ new Date(0)).toISOString();
10320
+ function carriedPlay(base, now, kind) {
10321
+ return advancePlay(base?.play, NO_READ, now, kind);
10322
+ }
10323
+ function liveWithLastGoal(matches) {
10324
+ return matches.map((m) => ({ ...m, lastGoal: provenLastGoal(m) }));
10325
+ }
10326
+ function playAfterRead(play, r, at, kind) {
10327
+ const whole = !r.degraded && r.complete === true;
10328
+ return advancePlay(play, { whole, matches: r.matches, finished: r.finished ?? [], finishedTruncated: r.finishedTruncated === true }, at, kind);
10329
+ }
9786
10330
  function liveAdapter(source, competition, now) {
9787
10331
  return makeAdapter(source, { competition, enrichGroups: false, now });
9788
10332
  }
@@ -9808,7 +10352,9 @@ async function runRefresh(opts) {
9808
10352
  degraded: true,
9809
10353
  // no live provider served this scope — never claim otherwise
9810
10354
  source,
9811
- competition
10355
+ competition,
10356
+ play: { seen: [], finished: [] }
10357
+ // nothing was read, and no snapshot to carry one from
9812
10358
  },
9813
10359
  idle
9814
10360
  );
@@ -9843,7 +10389,9 @@ async function runRefresh(opts) {
9843
10389
  degraded: until !== void 0,
9844
10390
  source,
9845
10391
  competition,
9846
- ...until !== void 0 ? { backoffUntil: new Date(until).toISOString() } : {}
10392
+ ...until !== void 0 ? { backoffUntil: new Date(until).toISOString() } : {},
10393
+ play: { seen: [], finished: [] }
10394
+ // nothing was read, and no snapshot to carry one from
9847
10395
  },
9848
10396
  idle,
9849
10397
  nowMs
@@ -9885,6 +10433,8 @@ async function runRefresh(opts) {
9885
10433
  let liveComplete = base ? base.liveComplete : false;
9886
10434
  let degraded = base?.degraded ?? false;
9887
10435
  let updatedAt = base?.updatedAt ?? now.toISOString();
10436
+ const kind = teamKind(competition);
10437
+ let play = carriedPlay(base, now, kind);
9888
10438
  let fixtures = base?.fixtures;
9889
10439
  let fixturesUpdatedAt = base?.fixturesUpdatedAt;
9890
10440
  let fixturesAttemptedAt = base?.fixturesAttemptedAt;
@@ -9895,10 +10445,11 @@ async function runRefresh(opts) {
9895
10445
  const adapter = liveAdapter(source, competition, clock);
9896
10446
  if (needLive) {
9897
10447
  try {
9898
- const r = await getLiveRead(adapter, now);
9899
- live = r.matches;
10448
+ const r = await getLiveRead(adapter, now, play.seen.map((e) => e.id));
10449
+ live = liveWithLastGoal(r.matches);
9900
10450
  liveComplete = r.complete === true;
9901
10451
  degraded = r.degraded;
10452
+ play = playAfterRead(play, r, now, kind);
9902
10453
  if (!r.degraded) season = r.season;
9903
10454
  } catch {
9904
10455
  liveComplete = false;
@@ -9946,7 +10497,8 @@ async function runRefresh(opts) {
9946
10497
  ...backoffUntil ? { backoffUntil } : {},
9947
10498
  ...season ? { season } : {},
9948
10499
  // Stored with the slice it describes, and only while that slice exists.
9949
- ...fixtures && fixturesSeason ? { fixturesSeason } : {}
10500
+ ...fixtures && fixturesSeason ? { fixturesSeason } : {},
10501
+ play
9950
10502
  },
9951
10503
  token,
9952
10504
  // The cycle's clock, not the carried live stamp: the throttle a
@@ -10028,6 +10580,8 @@ async function refreshOffBundle(c) {
10028
10580
  let liveComplete = base ? base.liveComplete : false;
10029
10581
  let degraded = base?.degraded ?? false;
10030
10582
  let updatedAt = base?.updatedAt ?? NEVER;
10583
+ const kind = teamKind(competition);
10584
+ let play = carriedPlay(base, now, kind);
10031
10585
  let season = sealSeason(base?.season);
10032
10586
  let index = view.index;
10033
10587
  let display = [...sealFixtures(base?.schedule?.fixtures, teamKind(competition)).items];
@@ -10057,7 +10611,8 @@ async function refreshOffBundle(c) {
10057
10611
  ...scheduleSeason ? { season: scheduleSeason } : {},
10058
10612
  ...inPlayUntil !== void 0 ? { inPlayUntil: new Date(inPlayUntil).toISOString() } : {},
10059
10613
  ...probe ? { probe: true } : {}
10060
- }
10614
+ },
10615
+ play
10061
10616
  });
10062
10617
  if (needDiscovery) {
10063
10618
  const before = { attemptedAt, failures };
@@ -10096,10 +10651,11 @@ async function refreshOffBundle(c) {
10096
10651
  if (ageMs(base, nowMs) >= MIN_REFRESH_MS) {
10097
10652
  const liveAt = clock();
10098
10653
  try {
10099
- const r = await getLiveRead(adapter, now);
10100
- live = r.matches;
10654
+ const r = await getLiveRead(adapter, now, play.seen.map((e) => e.id));
10655
+ live = liveWithLastGoal(r.matches);
10101
10656
  liveComplete = r.complete === true;
10102
10657
  degraded = r.degraded;
10658
+ play = playAfterRead(play, r, new Date(liveAt), kind);
10103
10659
  if (!r.degraded) season = r.season;
10104
10660
  if (r.matches.length > 0) inPlayUntil = raiseInPlay(inPlayUntil, r.matches, nowMs);
10105
10661
  else if (!r.degraded && r.complete) inPlayUntil = void 0;
@@ -10484,7 +11040,8 @@ function adapterServing({ cfg, adapter, now }, competition) {
10484
11040
  }
10485
11041
  var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
10486
11042
  var MARKETS_CMD_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
10487
- async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeMarketProvider) {
11043
+ async function marketSignalsFor(ctx, matches, baseOpts, now, providerFactory = makeMarketProvider) {
11044
+ const opts = { ...baseOpts, now };
10488
11045
  if (ctx.marketProvider) {
10489
11046
  const b = await getMarketSignals(ctx.marketProvider, matches, opts);
10490
11047
  return { signals: resolvedValues(b), complete: b.complete };
@@ -10495,7 +11052,12 @@ async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeM
10495
11052
  return { signals: resolvedValues(b), complete: b.complete };
10496
11053
  }
10497
11054
  const competition = ctx.cfg.competition;
10498
- const { signals: cached, checked: cachedIds } = readMarketCache("polymarket", competition);
11055
+ const { signals: cached, checked: cachedIds } = readMarketCache(
11056
+ "polymarket",
11057
+ competition,
11058
+ Date.now(),
11059
+ now
11060
+ );
10499
11061
  const result = /* @__PURE__ */ new Map();
10500
11062
  const miss = [];
10501
11063
  for (const m of matches) {
@@ -10517,12 +11079,11 @@ async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeM
10517
11079
  }
10518
11080
  return { signals: result, complete };
10519
11081
  }
10520
- async function reliableMarketSignals(ctx, matches) {
11082
+ async function reliableMarketSignals(ctx, matches, now) {
10521
11083
  if (ctx.cfg.markets === false) return { signals: /* @__PURE__ */ new Map(), complete: true };
10522
- const now = ctx.now ?? /* @__PURE__ */ new Date();
10523
11084
  const relevant = matches.filter((m) => marketRelevant(m, now));
10524
11085
  if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
10525
- const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS);
11086
+ const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS, now);
10526
11087
  const out2 = /* @__PURE__ */ new Map();
10527
11088
  for (const [id, s] of raw.signals) {
10528
11089
  const m = relevant.find((x) => x.id === id);
@@ -10530,8 +11091,8 @@ async function reliableMarketSignals(ctx, matches) {
10530
11091
  }
10531
11092
  return { signals: out2, complete: raw.complete };
10532
11093
  }
10533
- async function reliableMarketSignalFor(ctx, match) {
10534
- const result = await reliableMarketSignals(ctx, [match]);
11094
+ async function reliableMarketSignalFor(ctx, match, now) {
11095
+ const result = await reliableMarketSignals(ctx, [match], now);
10535
11096
  return { signal: result.signals.get(match.id), complete: result.complete };
10536
11097
  }
10537
11098
  function out(line = "") {
@@ -10541,7 +11102,10 @@ function emitJson(data) {
10541
11102
  out(JSON.stringify(data, null, 2));
10542
11103
  }
10543
11104
  function modeOut(cfg, c) {
10544
- out(c.dim(` ${modeLine(cfg.selection, cfg.lang)}`));
11105
+ out(modeRow(cfg, c));
11106
+ }
11107
+ function modeRow(cfg, c) {
11108
+ return c.dim(` ${modeLine(cfg.selection, cfg.lang)}`);
10545
11109
  }
10546
11110
  function competitionKey(cfg) {
10547
11111
  return selectionExtras(cfg.selection);
@@ -10665,11 +11229,12 @@ async function cmdToday(date, ctx) {
10665
11229
  const { cfg, t: t2 } = ctx;
10666
11230
  precheck(cfg, t2, date);
10667
11231
  const adapter = adapterFor(ctx);
10668
- const targetDate = date ?? localDate((ctx.now ?? /* @__PURE__ */ new Date()).toISOString(), cfg.tz);
11232
+ const now = requestClock(adapter, ctx.now);
11233
+ const targetDate = date ?? localDate(now.toISOString(), cfg.tz);
10669
11234
  const day = await getMatchesForDate(adapter, targetDate, resolveTz(cfg.tz));
10670
11235
  const { matches, degraded, source } = day;
10671
11236
  const todays = fixturesByDate(targetDate, matches, cfg.tz);
10672
- const market = await reliableMarketSignals(ctx, todays);
11237
+ const market = await reliableMarketSignals(ctx, todays, now);
10673
11238
  if (cfg.json) {
10674
11239
  emitJson({
10675
11240
  date: targetDate,
@@ -10721,13 +11286,14 @@ async function cmdToday(date, ctx) {
10721
11286
  const src = attribution.attributed ? dataSource(source, cfg.lang, c) : "";
10722
11287
  if (src) out(src);
10723
11288
  out(disclaimer(t2, c));
10724
- endScoreCommand(ctx);
11289
+ endScoreCommand(ctx, now);
10725
11290
  }
10726
11291
  async function cmdLive(ctx) {
10727
11292
  const { cfg, t: t2 } = ctx;
10728
11293
  precheck(cfg, t2);
10729
11294
  const adapter = adapterFor(ctx);
10730
- const live = await getLiveMatches(adapter, ctx.now ?? /* @__PURE__ */ new Date());
11295
+ const now = requestClock(adapter, ctx.now);
11296
+ const live = await getLiveMatches(adapter, now);
10731
11297
  const { matches, degraded, source } = live;
10732
11298
  if (cfg.json) {
10733
11299
  emitJson({ degraded, source: source ?? null, matches, ...verdictExtras(live), ...competitionKey(cfg) });
@@ -10755,7 +11321,7 @@ async function cmdLive(ctx) {
10755
11321
  const src = dataSource(source, cfg.lang, c);
10756
11322
  if (src) out(src);
10757
11323
  out(disclaimer(t2, c));
10758
- endScoreCommand(ctx);
11324
+ endScoreCommand(ctx, now);
10759
11325
  }
10760
11326
  async function cmdNext(team, ctx) {
10761
11327
  const { cfg, t: t2 } = ctx;
@@ -10796,7 +11362,7 @@ async function cmdNext(team, ctx) {
10796
11362
  }
10797
11363
  out();
10798
11364
  out(disclaimer(t2, c));
10799
- endScoreCommand(ctx);
11365
+ endScoreCommand(ctx, now);
10800
11366
  return;
10801
11367
  }
10802
11368
  for (const q of qualifiers) out(c.dim(" " + q));
@@ -10814,7 +11380,7 @@ async function cmdNext(team, ctx) {
10814
11380
  const src = dataSource(source, cfg.lang, c);
10815
11381
  if (src) out(src);
10816
11382
  out(disclaimer(t2, c));
10817
- endScoreCommand(ctx);
11383
+ endScoreCommand(ctx, now);
10818
11384
  }
10819
11385
  function cmdTeam(query, ctx) {
10820
11386
  const { cfg, t: t2 } = ctx;
@@ -11080,6 +11646,7 @@ function cmdAmbient({ cfg, now }, opts = {}) {
11080
11646
  }
11081
11647
  const { compact, max } = promptLineEnv();
11082
11648
  const preference = ambientPreference(cfg);
11649
+ const lang = normalizeLang(cfg.lang);
11083
11650
  const state = readCurrentState(cfg.source, cfg.competition);
11084
11651
  const read = ambientView(state, {
11085
11652
  pick: preference.pick,
@@ -11091,26 +11658,37 @@ function cmdAmbient({ cfg, now }, opts = {}) {
11091
11658
  defaultCompetition: bundleApplies(cfg.competition),
11092
11659
  teamKind: teamKind(cfg.competition),
11093
11660
  now,
11094
- columns: opts.columns
11661
+ columns: opts.columns,
11662
+ // The line's own tokens in the CLI's resolved language (the statusline module reads no environment).
11663
+ lang
11095
11664
  });
11096
11665
  const pick2 = preference.pick;
11097
11666
  const view = {
11098
11667
  line: read.line,
11668
+ lineParts: read.lineParts,
11099
11669
  idle: read.idle,
11100
11670
  empty: read.empty,
11101
11671
  context: read.context,
11102
11672
  live: read.live,
11673
+ finished: read.finished,
11103
11674
  current: read.current,
11104
11675
  next: read.next,
11105
11676
  pick: pick2 === void 0 ? null : "code" in pick2 ? { code: pick2.code } : pick2.team,
11106
11677
  ...preference.unreadable ? { pickUnreadable: true } : {},
11107
11678
  // A selected competition: core's key is present (see `selectionExtras`).
11108
11679
  ...selectionExtras(cfg.selection),
11680
+ // The language of the line and of the plugin's moment words (core's `moment.*` keys, templates).
11681
+ lang,
11682
+ words: momentWords(lang),
11109
11683
  degraded: read.degraded,
11110
11684
  source: read.source,
11111
11685
  updatedAt: read.updatedAt,
11112
11686
  staleAfter: read.staleAfter,
11113
- disclaimer: DISCLAIMER
11687
+ disclaimer: DISCLAIMER,
11688
+ // The provider's sentence, English as every ambient string (never the reader's language): when the snapshot
11689
+ // names a provider and the line is neither the sign-off (`idle`) nor nothing known (`empty`); none otherwise.
11690
+ // `degraded` does not decide: a degraded snapshot still shows what an earlier read or discovery served.
11691
+ attribution: read.source !== null && !read.idle && !read.empty ? t("en", "live.data", { source: liveSourceLabel(read.source) }) : null
11114
11692
  };
11115
11693
  out(JSON.stringify(view));
11116
11694
  printed = true;
@@ -11214,8 +11792,11 @@ async function cmdMatch(id, ctx) {
11214
11792
  const now = requestClock(adapter, ctx.now);
11215
11793
  const found = await getMatchById(adapter, id, now);
11216
11794
  const { match, degraded, source: liveSource } = found;
11217
- const market = match ? await reliableMarketSignalFor({ ...ctx, now }, match) : { signal: void 0, complete: true };
11795
+ const market = match ? await reliableMarketSignalFor(ctx, match, now) : { signal: void 0, complete: true };
11796
+ const answer = matchAnswerRows(found, id, market, cfg, t2);
11218
11797
  if (cfg.json) {
11798
+ const rows = [...answer.rows];
11799
+ while (rows.length && rowText(rows[rows.length - 1]) === "") rows.pop();
11219
11800
  emitJson({
11220
11801
  degraded,
11221
11802
  match: match ?? null,
@@ -11227,59 +11808,91 @@ async function cmdMatch(id, ctx) {
11227
11808
  // The span a whole read searched for an id it did not hold (a plain field).
11228
11809
  ...found.window ? { window: found.window } : {},
11229
11810
  ...verdictExtras(found),
11811
+ lines: rows.map(rowText),
11812
+ rows,
11813
+ attribution: answer.attribution || null,
11814
+ disclaimer: answer.disclaimer,
11230
11815
  ...competitionKey(cfg)
11231
11816
  });
11232
11817
  return;
11233
11818
  }
11234
11819
  const c = painterFor(cfg);
11235
- const qualifiers = verdictQualifiers(found, cfg.lang);
11236
11820
  out();
11821
+ for (const row of answer.rows) out(paintRow(row, c));
11822
+ if (answer.attribution) out(c.dim(answer.attribution));
11823
+ out(c.dim(answer.disclaimer));
11824
+ if (match) maybeStarNudge(ctx);
11825
+ }
11826
+ function rowText(row) {
11827
+ return row.parts.map((p) => p.text).join("");
11828
+ }
11829
+ function paintPart(p, c) {
11830
+ switch (p.style) {
11831
+ case "strong":
11832
+ return c.bold(p.text);
11833
+ case "dim":
11834
+ case "quote":
11835
+ return c.dim(p.text);
11836
+ case "accent":
11837
+ case "flair":
11838
+ return c.green(p.text);
11839
+ default:
11840
+ return p.text;
11841
+ }
11842
+ }
11843
+ function paintRow(row, c) {
11844
+ return row.parts.map((p) => paintPart(p, c)).join("");
11845
+ }
11846
+ function matchAnswerRows(found, id, market, cfg, t2) {
11847
+ const { match, degraded, source: liveSource } = found;
11848
+ const rows = [];
11849
+ const row = (role, ...parts) => rows.push({ role, parts });
11850
+ const blank = (role = "blank") => rows.push({ role, parts: [] });
11851
+ const plain2 = painterFor({ ...cfg, color: false });
11852
+ const footer = { disclaimer: disclaimer(t2, plain2) };
11853
+ const mode = { text: modeRow(cfg, plain2), style: "dim" };
11854
+ const qualifiers = verdictQualifiers(found, cfg.lang);
11237
11855
  if (!match) {
11238
- modeOut(cfg, c);
11239
- for (const q of qualifiers) out(c.dim(" " + q));
11240
- out(
11241
- c.dim(
11242
- " " + (degraded ? t2("live.degraded") : verdictNotice(found, cfg.lang) ?? matchWindowSentence(found, id, cfg.lang) ?? matchNoneReadSentence(found, id, cfg.lang) ?? t2("match.none", { id }))
11243
- )
11244
- );
11245
- out();
11246
- out(disclaimer(t2, c));
11247
- return;
11856
+ row("text", mode);
11857
+ for (const q of qualifiers) row("text", { text: " " + q, style: "dim" });
11858
+ row("text", {
11859
+ text: " " + (degraded ? t2("live.degraded") : verdictNotice(found, cfg.lang) ?? matchWindowSentence(found, id, cfg.lang) ?? matchNoneReadSentence(found, id, cfg.lang) ?? t2("match.none", { id })),
11860
+ style: "dim"
11861
+ });
11862
+ blank("text");
11863
+ return { rows, attribution: "", ...footer };
11248
11864
  }
11249
- for (const q of qualifiers) out(c.dim(" " + q));
11865
+ for (const q of qualifiers) row("qualifier", { text: " " + q, style: "dim" });
11250
11866
  const attribution = dayAttribution(found, [match], cfg.lang);
11251
- if (attribution.unserved) out(c.dim(" " + attribution.unserved));
11252
- out(header(`${match.home.name} ${scoreline(match)} ${match.away.name}`, c));
11253
- modeOut(cfg, c);
11867
+ if (attribution.unserved) row("qualifier", { text: " " + attribution.unserved, style: "dim" });
11868
+ const pin = cfg.pin;
11869
+ const side = (team) => pin !== void 0 && isPinnedSide(team, pin) ? { text: team.name, style: "accent" } : { text: team.name };
11870
+ row("title", side(match.home), { text: " " }, { text: scoreline(match), style: "strong" }, { text: " " }, side(match.away));
11871
+ row("mode", mode);
11254
11872
  const where = joinSegments([stageLabelI18n(cfg.lang, match), matchLocation(match)]);
11255
- if (where) out(" " + c.dim(where));
11256
- out(
11257
- " " + c.dim(
11258
- `${formatKickoff(match.kickoff, { tz: cfg.tz, locale: cfg.lang })} ${statusToken(match, t2, c)}`.trimEnd()
11259
- )
11260
- );
11873
+ if (where) row("meta", { text: " " }, { text: where, style: "dim" });
11874
+ const word = statusWord(match, t2);
11875
+ const kickoff = { text: `${t2("match.kickoff")} ${formatKickoff(match.kickoff, { tz: cfg.tz, locale: cfg.lang })}`, style: "dim" };
11876
+ if (word) row("status", { text: " " }, { text: word, style: "strong" }, { text: " \xB7 ", style: "dim" }, kickoff);
11877
+ else row("status", { text: " " }, kickoff);
11261
11878
  const flair = matchFlair(match, flairOpts(cfg));
11262
- if (flair.text) out(" " + (flair.rally ? c.green(flair.text) : c.cyan(flair.text)));
11263
- if (match.events?.length) {
11264
- out();
11265
- for (const e of match.events) {
11266
- out(` ${e.minute}' ${e.type} ${e.teamCode}${e.player ? ` \u2014 ${e.player}` : ""}`);
11267
- }
11268
- }
11879
+ if (flair.text) row("flair", { text: " " }, { text: flair.text, style: flair.rally ? "flair" : "quote" });
11269
11880
  if (market.signal) {
11270
- out();
11271
- for (const mline of marketBlock(market.signal, match)) out(" " + c.dim(mline));
11881
+ blank();
11882
+ for (const mline of marketBlock(market.signal, match)) row("market", { text: " " }, { text: mline, style: "dim" });
11883
+ }
11884
+ const timeline = timelineParts(match, { lang: cfg.lang, flags: flagsEnabled() });
11885
+ if (timeline.length) {
11886
+ blank();
11887
+ for (const parts of timeline) row("event", ...parts);
11272
11888
  }
11273
11889
  if (!market.complete) {
11274
- out();
11275
- out(c.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
11890
+ blank();
11891
+ row("note", { text: " Market data unavailable or incomplete \u2014 this match could not be checked.", style: "dim" });
11276
11892
  }
11277
- out();
11278
- if (degraded) out(c.dim(" " + t2(found.earlierRecord ? "feed.earlierRecord" : "feed.degraded")));
11279
- const src = dataSource(liveSource, cfg.lang, c);
11280
- if (src) out(src);
11281
- out(disclaimer(t2, c));
11282
- maybeStarNudge(ctx);
11893
+ blank();
11894
+ if (degraded) row("note", { text: " " + t2(found.earlierRecord ? "feed.earlierRecord" : "feed.degraded"), style: "dim" });
11895
+ return { rows, attribution: dataSource(liveSource, cfg.lang, plain2), ...footer };
11283
11896
  }
11284
11897
  var MARKET_INFO = "Prediction-market data is informational only.";
11285
11898
  function marketHeaderLine(m, cfg) {
@@ -11302,10 +11915,11 @@ async function cmdMarkets(target, team, ctx) {
11302
11915
  const asked = teamAsked(team, cfg);
11303
11916
  if (!asked) throw new InputError(usage);
11304
11917
  const code = "pin" in asked ? asked.pin.code : resolveTeamArg(asked.query, usage, t2, cfg.competition);
11305
- const now2 = ctx.now ?? /* @__PURE__ */ new Date();
11306
- const picked = await marketFixtureForTeam(adapterFor(ctx), code, now2);
11918
+ const adapter2 = adapterFor(ctx);
11919
+ const now2 = requestClock(adapter2, ctx.now);
11920
+ const picked = await marketFixtureForTeam(adapter2, code, now2);
11307
11921
  const { match: fixture, degraded } = picked;
11308
- const market = fixture && marketRelevant(fixture, now2) ? await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
11922
+ const market = fixture && marketRelevant(fixture, now2) ? await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS, now2) : { signals: /* @__PURE__ */ new Map(), complete: true };
11309
11923
  const sig = fixture ? market.signals.get(fixture.id) : void 0;
11310
11924
  const shown = market.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
11311
11925
  if (cfg.json) {
@@ -11372,11 +11986,11 @@ async function cmdMarkets(target, team, ctx) {
11372
11986
  out(c3.dim(MARKET_INFO));
11373
11987
  return;
11374
11988
  }
11375
- const adapter = adapterFor(ctx);
11376
- const now2 = requestClock(adapter, ctx.now);
11377
- const found = await getMatchById(adapter, target, now2);
11989
+ const adapter2 = adapterFor(ctx);
11990
+ const now2 = requestClock(adapter2, ctx.now);
11991
+ const found = await getMatchById(adapter2, target, now2);
11378
11992
  const { match } = found;
11379
- const market = match && marketRelevant(match, now2) ? await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
11993
+ const market = match && marketRelevant(match, now2) ? await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS, now2) : { signals: /* @__PURE__ */ new Map(), complete: true };
11380
11994
  const sig = match ? market.signals.get(match.id) : void 0;
11381
11995
  const shown = market.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
11382
11996
  if (cfg.json) {
@@ -11412,12 +12026,13 @@ async function cmdMarkets(target, team, ctx) {
11412
12026
  }
11413
12027
  const explicitDate = target && target !== "today" ? target : void 0;
11414
12028
  precheck(cfg, t2, explicitDate);
11415
- const now = ctx.now ?? /* @__PURE__ */ new Date();
12029
+ const adapter = adapterFor(ctx);
12030
+ const now = requestClock(adapter, ctx.now);
11416
12031
  const date = explicitDate ?? localDate(now.toISOString(), cfg.tz);
11417
- const day = await getMatchesForDate(adapterFor(ctx), date, resolveTz(cfg.tz));
12032
+ const day = await getMatchesForDate(adapter, date, resolveTz(cfg.tz));
11418
12033
  const todays = fixturesByDate(date, day.matches, cfg.tz);
11419
12034
  const relevant = todays.filter((m) => marketRelevant(m, now));
11420
- const { signals, complete } = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
12035
+ const { signals, complete } = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS, now);
11421
12036
  const rows = relevant.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
11422
12037
  (r) => !!r.signal && marketDisplayable(r.match, r.signal)
11423
12038
  );
@@ -11466,8 +12081,8 @@ async function cmdMarkets(target, team, ctx) {
11466
12081
  function pickShareStyle(v) {
11467
12082
  return v === "compact" ? "compact" : "social";
11468
12083
  }
11469
- async function reliableShareSignals(ctx, matches) {
11470
- const raw = await reliableMarketSignals(ctx, matches);
12084
+ async function reliableShareSignals(ctx, matches, now) {
12085
+ const raw = await reliableMarketSignals(ctx, matches, now);
11471
12086
  const out2 = /* @__PURE__ */ new Map();
11472
12087
  for (const [id, s] of raw.signals) {
11473
12088
  const m = matches.find((x) => x.id === id);
@@ -11568,7 +12183,8 @@ async function cmdShare(target, team, opts, ctx) {
11568
12183
  const where = { tz: cfg.tz, locale: cfg.lang, competition: cfg.competition };
11569
12184
  if (target === "live") {
11570
12185
  precheck(cfg, t2);
11571
- const live = await getLiveMatches(adapterFor(ctx), ctx.now ?? /* @__PURE__ */ new Date());
12186
+ const adapter2 = adapterFor(ctx);
12187
+ const live = await getLiveMatches(adapter2, requestClock(adapter2, ctx.now));
11572
12188
  emitMatchCard(ctx, liveShareCard(live, where), { ...baseOptions, includeMarkets: false }, copy);
11573
12189
  return;
11574
12190
  }
@@ -11609,27 +12225,29 @@ async function cmdShare(target, team, opts, ctx) {
11609
12225
  }
11610
12226
  if (target === "next") {
11611
12227
  precheck(cfg, t2);
11612
- const { code, next, now } = await nextAsked(ctx, team, teamUsage("share next", cfg));
11613
- const market2 = await reliableShareSignals({ ...ctx, now }, next.fixture ? [next.fixture] : []);
12228
+ const { code, next, now: now2 } = await nextAsked(ctx, team, teamUsage("share next", cfg));
12229
+ const market2 = await reliableShareSignals(ctx, next.fixture ? [next.fixture] : [], now2);
11614
12230
  emitMatchCard(ctx, nextShareCard(next, code, market2, where), baseOptions, copy);
11615
12231
  return;
11616
12232
  }
11617
12233
  if (target && target !== "today" && !isValidDate(target)) {
11618
12234
  precheck(cfg, t2);
11619
- const adapter = adapterFor(ctx);
11620
- const now = requestClock(adapter, ctx.now);
11621
- const found = await getMatchById(adapter, target, now);
11622
- const market2 = await reliableShareSignals({ ...ctx, now }, found.match ? [found.match] : []);
12235
+ const adapter2 = adapterFor(ctx);
12236
+ const now2 = requestClock(adapter2, ctx.now);
12237
+ const found = await getMatchById(adapter2, target, now2);
12238
+ const market2 = await reliableShareSignals(ctx, found.match ? [found.match] : [], now2);
11623
12239
  emitMatchCard(ctx, matchShareCard(found, target, market2, where), baseOptions, copy);
11624
12240
  return;
11625
12241
  }
11626
12242
  const explicitDate = target && target !== "today" ? target : void 0;
11627
12243
  precheck(cfg, t2, explicitDate);
11628
- const date = explicitDate ?? localDate((ctx.now ?? /* @__PURE__ */ new Date()).toISOString(), cfg.tz);
11629
- const day = await getMatchesForDate(adapterFor(ctx), date, resolveTz(cfg.tz));
12244
+ const adapter = adapterFor(ctx);
12245
+ const now = requestClock(adapter, ctx.now);
12246
+ const date = explicitDate ?? localDate(now.toISOString(), cfg.tz);
12247
+ const day = await getMatchesForDate(adapter, date, resolveTz(cfg.tz));
11630
12248
  const { matches: all, degraded, source } = day;
11631
12249
  const todays = fixturesByDate(date, all, cfg.tz);
11632
- const market = await reliableShareSignals(ctx, todays);
12250
+ const market = await reliableShareSignals(ctx, todays, now);
11633
12251
  emitMatchCard(
11634
12252
  ctx,
11635
12253
  dateShareCard(
@@ -11717,7 +12335,8 @@ async function pinFor(ctx, competition, query) {
11717
12335
  if (!isKnownSource(cfg.source)) {
11718
12336
  throw new InputError(t2("err.source", { source: cfg.source, sources: KNOWN_SOURCES.join(", ") }));
11719
12337
  }
11720
- const next = await getNextFixtureForTeam(adapterServing(ctx, competition), label2, ctx.now ?? /* @__PURE__ */ new Date());
12338
+ const adapter = adapterServing(ctx, competition);
12339
+ const next = await getNextFixtureForTeam(adapter, label2, requestClock(adapter, ctx.now));
11721
12340
  if (next.team) {
11722
12341
  const { id, code, name } = next.team;
11723
12342
  return id !== void 0 ? { id, code, name } : { code, name };
@@ -11910,8 +12529,8 @@ function maybeStarNudge(ctx) {
11910
12529
  out();
11911
12530
  out(c.dim(` \u2B50 Enjoying Claudinho? Star it \u2192 ${REPO_URL} (claudinho star)`));
11912
12531
  }
11913
- function endScoreCommand(ctx) {
11914
- const over = bundleApplies(ctx.cfg.competition) && isTournamentWindowOver(ctx.now?.getTime() ?? Date.now());
12532
+ function endScoreCommand(ctx, now) {
12533
+ const over = bundleApplies(ctx.cfg.competition) && isTournamentWindowOver(now.getTime());
11915
12534
  if (over) printTournamentSignOff(ctx);
11916
12535
  else maybeStarNudge(ctx);
11917
12536
  }
@@ -11935,7 +12554,8 @@ function printInitStarCta(cfg) {
11935
12554
  function cmdVibe(ctx) {
11936
12555
  const { cfg } = ctx;
11937
12556
  const bundled = cfg.selection.kind === "selected" && bundleApplies(cfg.competition);
11938
- const pool = vibePool(localDate((ctx.now ?? /* @__PURE__ */ new Date()).toISOString(), cfg.tz), bundled);
12557
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
12558
+ const pool = vibePool(localDate(now.toISOString(), cfg.tz), bundled);
11939
12559
  const line = pool[Math.floor(Math.random() * pool.length)];
11940
12560
  let liveSeg;
11941
12561
  try {
@@ -11943,7 +12563,7 @@ function cmdVibe(ctx) {
11943
12563
  const state = readCurrentState(cfg.source, cfg.competition);
11944
12564
  liveSeg = vibeLiveSegment(
11945
12565
  // Sealed with the competition's written kind, like the statusline's.
11946
- liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime(), teamKind(cfg.competition)).items,
12566
+ liveMatchesFromCache(state, now.getTime(), teamKind(cfg.competition)).items,
11947
12567
  // The statusline's and the hook's pick (offline).
11948
12568
  ambientPick(cfg)
11949
12569
  );
@@ -11969,7 +12589,7 @@ function handlePipeError(stream) {
11969
12589
  }
11970
12590
  handlePipeError(process.stdout);
11971
12591
  handlePipeError(process.stderr);
11972
- var VERSION = "0.11.2";
12592
+ var VERSION = "0.12.0";
11973
12593
  var DISCLAIMER2 = `Claudinho is an ${FAN_PROJECT.toLowerCase()}. ${DISCLAIMER}`;
11974
12594
  function ctxFrom(cmd) {
11975
12595
  let root = cmd;