@claudinho/cli 0.11.1 → 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 +326 -249
  2. package/dist/index.js +1085 -200
  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",
@@ -762,7 +855,7 @@ function formatTime(iso, opts = {}) {
762
855
  }).format(when);
763
856
  }
764
857
  var COUNTDOWN_NOW = "now";
765
- function countdown(iso, from = /* @__PURE__ */ new Date()) {
858
+ function countdown(iso, from) {
766
859
  const when = parsedDate(iso);
767
860
  if (!when) return UNKNOWN_TIME;
768
861
  const ms = when.getTime() - from.getTime();
@@ -775,7 +868,7 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
775
868
  if (hours > 0) return `${hours}h${mins}m`;
776
869
  return `${mins}m`;
777
870
  }
778
- function countdownPhrase(lang, iso, from = /* @__PURE__ */ new Date()) {
871
+ function countdownPhrase(lang, iso, from) {
779
872
  const left = countdown(iso, from);
780
873
  return left === COUNTDOWN_NOW ? t(lang, "countdown.now") : t(lang, "next.in", { countdown: left });
781
874
  }
@@ -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.1"} (+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;
@@ -6237,6 +6481,9 @@ function mergeLive(base, live) {
6237
6481
  function clockOf(adapter) {
6238
6482
  return new Date(adapter.now?.() ?? Date.now());
6239
6483
  }
6484
+ function requestClock(adapter, given) {
6485
+ return given ?? clockOf(adapter);
6486
+ }
6240
6487
  function providerDayOf(adapter, instant) {
6241
6488
  if (Number.isNaN(instant.getTime())) return "";
6242
6489
  return adapter.bucketDay?.(instant) || instant.toISOString().slice(0, 10);
@@ -6411,7 +6658,7 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
6411
6658
  });
6412
6659
  let refreshRead = {};
6413
6660
  if (candidate) {
6414
- const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id);
6661
+ const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id, now);
6415
6662
  refreshRead = partial ? { partial } : {};
6416
6663
  const m = r.degraded ? candidate : r.match ?? candidate;
6417
6664
  if (!isFinished(m.status)) return { ...r, match: m, ...bothReads(windowRead, refreshRead) };
@@ -6644,10 +6891,19 @@ async function getScheduleAhead(adapter, now = /* @__PURE__ */ new Date()) {
6644
6891
  };
6645
6892
  }
6646
6893
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
6647
- const { complete: _complete, ...result } = await getLiveRead(adapter, now);
6894
+ const { complete: _complete, finished: _finished, finishedTruncated: _truncated, ...result } = await getLiveRead(adapter, now);
6648
6895
  return result;
6649
6896
  }
6650
- 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 = []) {
6651
6907
  try {
6652
6908
  const day = now.toISOString().slice(0, 10);
6653
6909
  const fetched = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1), { acrossSeasons: true }) : await adapter.fetchLive();
@@ -6655,6 +6911,7 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
6655
6911
  const season = meta?.season;
6656
6912
  const matches = fetched.filter((m) => isLive(m.status));
6657
6913
  const between = betweenEditionsOf(adapter, meta, fetched, providerDayOf(adapter, now));
6914
+ const done = finishedOf(fetched, seen);
6658
6915
  return {
6659
6916
  matches,
6660
6917
  degraded: false,
@@ -6666,10 +6923,12 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
6666
6923
  // boolean below is the refresher's, unchanged: false for that, for an
6667
6924
  // adapter that said nothing, and for a failed read alike.
6668
6925
  ...partialOfRead(meta),
6669
- complete: meta?.complete === true
6926
+ complete: meta?.complete === true,
6927
+ finished: done.finished,
6928
+ finishedTruncated: done.truncated
6670
6929
  };
6671
6930
  } catch {
6672
- return { matches: [], degraded: true, complete: false };
6931
+ return { matches: [], degraded: true, complete: false, finished: [], finishedTruncated: false };
6673
6932
  }
6674
6933
  }
6675
6934
  function norm2(s) {
@@ -6905,22 +7164,29 @@ function sealMarketSignal(raw, options = {}) {
6905
7164
  if (sourceMarketId) out2.sourceMarketId = sourceMarketId;
6906
7165
  out2.asOf = canonicalTimestamp(s.asOf) ?? "";
6907
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
+ }
6908
7175
  out2.outcomes = outcomes;
6909
7176
  const isAmbiguous = s.ambiguous !== false;
6910
7177
  const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
6911
7178
  if (favorite) out2.favorite = favorite;
6912
7179
  if (liquidity !== void 0) out2.liquidity = liquidity;
6913
7180
  if (volume24h !== void 0) out2.volume24h = volume24h;
6914
- out2.stale = s.stale !== false;
7181
+ out2.stale = isStaleSignal(out2, { now: judgedAt, maxAgeMs: options.maxAgeMs });
6915
7182
  out2.ambiguous = isAmbiguous || out2.source === "";
6916
- out2.stale = out2.stale || isStaleSignal(out2, { now: options.now, maxAgeMs: options.maxAgeMs });
6917
7183
  return valid(out2);
6918
7184
  }
6919
7185
  function parseCachedMarketSignal(raw, options = {}) {
6920
7186
  return sealMarketSignal(raw, options);
6921
7187
  }
6922
7188
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
6923
- function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
7189
+ function marketRelevant(match, now) {
6924
7190
  if (isLive(match.status)) return true;
6925
7191
  if (isFinished(match.status)) return false;
6926
7192
  const k = Date.parse(match.kickoff);
@@ -7009,12 +7275,13 @@ function buildMarketSignal(input) {
7009
7275
  const outcomes = normalizeOutcomes(input.outcomes);
7010
7276
  const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
7011
7277
  const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
7278
+ const clock = input.now ?? /* @__PURE__ */ new Date();
7012
7279
  const signal = {
7013
7280
  matchId: input.match.id,
7014
7281
  source: input.source,
7015
7282
  sourceMarketId: input.sourceMarketId,
7016
7283
  asOf: input.asOf,
7017
- fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7284
+ fetchedAt: input.fetchedAt ?? clock.toISOString(),
7018
7285
  outcomes,
7019
7286
  favorite,
7020
7287
  liquidity: input.liquidity,
@@ -7022,8 +7289,8 @@ function buildMarketSignal(input) {
7022
7289
  stale: false,
7023
7290
  ambiguous: ambiguous2
7024
7291
  };
7025
- signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
7026
- 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 });
7027
7294
  if (sealed.kind !== "valid") {
7028
7295
  return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
7029
7296
  }
@@ -8730,6 +8997,14 @@ function makeT(lang) {
8730
8997
  };
8731
8998
  }
8732
8999
 
9000
+ // src/options.ts
9001
+ import { InvalidArgumentError } from "commander";
9002
+ function positiveInteger(value) {
9003
+ const n = /^\d+$/.test(value) ? Number.parseInt(value, 10) : 0;
9004
+ if (!(n > 0)) throw new InvalidArgumentError("a positive integer");
9005
+ return n;
9006
+ }
9007
+
8733
9008
  // src/tagline.ts
8734
9009
  var NAMED = ["the World Cup", "the Premier League", "LALIGA", "the Champions League"];
8735
9010
  var TAGLINE = `Live football scores, fixtures and standings in your terminal and your Claude Code and Cursor CLI statusline, for the competition you follow: ${NAMED.join(", ")} and ${SUPPORTED.length - NAMED.length} more.`;
@@ -8769,17 +9044,33 @@ function painterFor(cfg) {
8769
9044
  return paint(cfg.color);
8770
9045
  }
8771
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) {
8772
9063
  switch (m.status) {
8773
9064
  case "LIVE":
8774
- return c.green(`${m.minute ? `${m.minute}'` : t2("status.live")}`);
9065
+ return m.minute ? `${m.minute}'` : t2("status.live");
8775
9066
  case "HT":
8776
- return c.yellow(t2("status.ht"));
9067
+ return t2("status.ht");
8777
9068
  case "FT":
8778
- return c.gray(t2("status.ft"));
9069
+ return t2("status.ft");
8779
9070
  case "POSTPONED":
8780
- return c.red(t2("status.postponed"));
9071
+ return t2("status.postponed");
8781
9072
  case "CANCELLED":
8782
- return c.red(t2("status.cancelled"));
9073
+ return t2("status.cancelled");
8783
9074
  default:
8784
9075
  return "";
8785
9076
  }
@@ -8969,7 +9260,9 @@ function readState(source, competition) {
8969
9260
  const bytes = readSmallFile(cachePath(source, competition), MAX_STATE_BYTES);
8970
9261
  if (!bytes) return void 0;
8971
9262
  const parsed = JSON.parse(bytes.toString("utf8"));
8972
- return isCacheState(parsed) ? parsed : void 0;
9263
+ if (!isCacheState(parsed)) return void 0;
9264
+ if (parsed.liveComplete !== void 0 && typeof parsed.liveComplete !== "boolean") delete parsed.liveComplete;
9265
+ return parsed;
8973
9266
  } catch {
8974
9267
  return void 0;
8975
9268
  }
@@ -9000,12 +9293,60 @@ function writeState(state, now = stampOrNow(state.updatedAt)) {
9000
9293
  const until = envelopeDeadline(replaced, now);
9001
9294
  if (until !== void 0 && !writeBackoffNote(state.source, state.competition, until, now)) return false;
9002
9295
  }
9003
- writeFileAtomic(
9004
- cachePath(state.source, state.competition),
9005
- JSON.stringify({ ...state, version: CACHE_VERSION })
9006
- );
9296
+ writeFileAtomic(cachePath(state.source, state.competition), boundedSnapshot(withoutEvents(state)));
9007
9297
  return true;
9008
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
+ }
9009
9350
  function stampOrNow(stamp) {
9010
9351
  return validStamp(stamp) ? Date.parse(stamp) : Date.now();
9011
9352
  }
@@ -9229,7 +9570,7 @@ function isUsableSignal(s) {
9229
9570
  if (!s.favorite) return false;
9230
9571
  return hasSaneDistribution(s.outcomes);
9231
9572
  }
9232
- function readMarketCache(source, competition, now = Date.now()) {
9573
+ function readMarketCache(source, competition, now = Date.now(), requestNow) {
9233
9574
  const signals = /* @__PURE__ */ new Map();
9234
9575
  const checked = /* @__PURE__ */ new Set();
9235
9576
  const file = readFile();
@@ -9256,7 +9597,7 @@ function readMarketCache(source, competition, now = Date.now()) {
9256
9597
  checked.add(id);
9257
9598
  continue;
9258
9599
  }
9259
- const sealed = parseCachedMarketSignal(entry.signal, { now: new Date(now) });
9600
+ const sealed = parseCachedMarketSignal(entry.signal, { now: requestNow });
9260
9601
  if (sealed.kind !== "valid") continue;
9261
9602
  const clean = sealed.value;
9262
9603
  if (clean.matchId !== id || clean.source !== source) continue;
@@ -9354,6 +9695,104 @@ function copyToClipboard(text, platform = process.platform) {
9354
9695
  return false;
9355
9696
  }
9356
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
+
9357
9796
  // src/scheduleSlice.ts
9358
9797
  var SCHEDULE_DISPLAY_MAX = 64;
9359
9798
  var RELEVANT_BACK_MS = 6 * 60 * 6e4;
@@ -9448,6 +9887,7 @@ function applyDiscovery(prev, answer, now) {
9448
9887
  // src/statusline.ts
9449
9888
  var DISPLAY_STALE_MS = 5 * 6e4;
9450
9889
  var TOURNAMENT_COMPLETE_LINE = "\u26BD World Cup 2026 is complete \xB7 claudinho follow --list";
9890
+ var NOTHING_KNOWN_LINE = "\u26BD \u2014";
9451
9891
  var FIRST_RUN_LINE = "\u26BD claudinho follow";
9452
9892
  function pickAmbientMatch(matches, pick2) {
9453
9893
  if (!pick2) return [...matches];
@@ -9482,8 +9922,17 @@ function isMatchShaped(m) {
9482
9922
  const x = m;
9483
9923
  return !!x && typeof x === "object" && typeof x.id === "string" && typeof x.kickoff === "string" && !!x.home?.code && !!x.away?.code;
9484
9924
  }
9485
- function upcomingResolved(now, fixtures = allFixtures()) {
9486
- 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)));
9487
9936
  }
9488
9937
  function defaultTeamKind(defaultCompetition) {
9489
9938
  return defaultCompetition === false ? "club" : "nation";
@@ -9491,14 +9940,38 @@ function defaultTeamKind(defaultCompetition) {
9491
9940
  function teamTok(t2, flags) {
9492
9941
  return flags && t2.flag ? t2.flag : t2.code;
9493
9942
  }
9494
- function matchSegment(m, compact, flags) {
9495
- const minute = m.status === "HT" ? "HT" : m.minute ? `${m.minute}'` : "LIVE";
9496
- if (!flags) {
9497
- 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;
9498
9972
  }
9499
- const home = compact ? teamTok(m.home, true) : withFlag(m.home.code, m.home.flag, "home");
9500
- const away = compact ? teamTok(m.away, true) : withFlag(m.away.code, m.away.flag, "away");
9501
- 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 }];
9502
9975
  }
9503
9976
  var MAX_LIVE_CONSIDERED = 64;
9504
9977
  var MAX_LIVE_EXAMINED = 512;
@@ -9536,13 +10009,17 @@ function sealFixtures(raw, kind) {
9536
10009
  };
9537
10010
  }
9538
10011
  function liveMatchesFromCache(state, nowMs, kind) {
10012
+ return liveRecordsFromCache(state, nowMs, kind).list;
10013
+ }
10014
+ function liveRecordsFromCache(state, nowMs, kind) {
9539
10015
  const fresh = state && ageMs(state, nowMs) < DISPLAY_STALE_MS;
9540
10016
  const rawLive = fresh ? state?.live : [];
9541
10017
  if (!Array.isArray(rawLive)) {
9542
- return { items: [], total: 0, shown: 0, truncated: false, complete: false };
10018
+ return { list: { items: [], total: 0, shown: 0, truncated: false, complete: false }, sources: [] };
9543
10019
  }
9544
10020
  const liveArr = rawLive;
9545
10021
  const out2 = [];
10022
+ const sources = [];
9546
10023
  let inspected = 0;
9547
10024
  let readable = true;
9548
10025
  for (let i = 0; i < liveArr.length; i++) {
@@ -9559,27 +10036,38 @@ function liveMatchesFromCache(state, nowMs, kind) {
9559
10036
  continue;
9560
10037
  }
9561
10038
  const sealed = parsedValue(parseCachedMatch(m, { events: false, teamKind: kind }));
9562
- if (sealed) out2.push(sealed);
9563
- else readable = false;
10039
+ if (sealed) {
10040
+ out2.push(sealed);
10041
+ sources.push(raw);
10042
+ } else readable = false;
9564
10043
  }
9565
10044
  const exhausted = inspected === liveArr.length;
9566
10045
  const complete = exhausted && readable;
9567
10046
  return {
9568
- items: out2,
9569
- total: out2.length,
9570
- shown: out2.length,
9571
- truncated: !exhausted,
9572
- // False when we stopped early or a record was unreadable. In either case an
9573
- // empty list must not render as the authoritative "nothing is on".
9574
- 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
9575
10057
  };
9576
10058
  }
9577
10059
  var DEFAULT_MAX_SEGMENTS = 8;
9578
10060
  var MAX_LINE_COLUMNS = 200;
9579
10061
  function renderPrompt(state, opts = {}) {
9580
- return truncateVisible(renderPromptLine(state, opts), MAX_LINE_COLUMNS);
10062
+ return promptLine(state, opts).line;
10063
+ }
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)) };
9581
10069
  }
9582
- function renderPromptLine(state, opts = {}) {
10070
+ function renderPromptLine(state, opts = {}, played) {
9583
10071
  const now = opts.now ?? /* @__PURE__ */ new Date();
9584
10072
  const nowMs = now.getTime();
9585
10073
  const defaultCompetition = opts.defaultCompetition ?? true;
@@ -9589,52 +10077,89 @@ function renderPromptLine(state, opts = {}) {
9589
10077
  const kind = opts.teamKind ?? defaultTeamKind(defaultCompetition);
9590
10078
  const liveList = liveMatchesFromCache(state, nowMs, kind);
9591
10079
  const live = liveList.items;
9592
- const cachedFixtureList = sealFixtures(defaultCompetition ? state?.fixtures : state?.schedule?.fixtures, kind);
9593
- const cachedFixtures = defaultCompetition ? [...cachedFixtureList.items] : cachedFixtureList.items.filter((m) => hasLiveWindow(m.status));
9594
- const bundle = defaultCompetition ? allFixtures() : [];
9595
- const schedule = cachedFixtures.length ? mergeLive(bundle, cachedFixtures) : defaultCompetition ? void 0 : [];
10080
+ const { cachedFixtures, schedule } = countdownSchedule(state, defaultCompetition, kind);
10081
+ const width = lineColumns(opts);
9596
10082
  if (live.length > 0) {
9597
10083
  const ordered = pickAmbientMatch(live, pick2);
9598
10084
  const picked = ordered[0] !== void 0 && isPicked(ordered[0], pick2);
9599
10085
  const max = picked ? 1 : opts.max && opts.max > 0 ? Math.min(opts.max, DEFAULT_MAX_SEGMENTS) : DEFAULT_MAX_SEGMENTS;
9600
10086
  const shown = ordered.slice(0, max);
9601
10087
  const overflow = live.length - shown.length;
9602
- const marker = !liveList.complete ? " +more" : overflow > 0 ? ` +${overflow}` : "";
9603
- const body = "\u26BD " + shown.map((m) => matchSegment(m, compact, flags)).join(" \xB7 ");
9604
- return truncateVisible(body, MAX_LINE_COLUMNS - displayWidth(marker)) + marker;
10088
+ return fitLiveLine(shown.map((m) => matchSegment(m, compact, flags, opts.lang)), overflow, liveList.complete, width);
9605
10089
  }
9606
- const cacheFresh = !!state && state.degraded !== true && ageMs(state, nowMs) < DISPLAY_STALE_MS;
9607
- if ((!cacheFresh || !liveList.complete) && !defaultCompetition) {
9608
- if (scheduleGateOpen(scheduleView(state?.schedule, nowMs), nowMs, { probe: false })) {
9609
- const win = pickAmbientMatch(
9610
- cachedFixtures.filter((m) => {
9611
- const k = Date.parse(m.kickoff);
9612
- return k <= nowMs && nowMs < k + LIVE_WINDOW_MS;
9613
- }).sort(byKickoff),
9614
- pick2
9615
- );
9616
- const first = win[0];
9617
- const matchup = first && isResolvedFixture(first) ? `${teamTok(first.home, flags)} vs ${teamTok(first.away, flags)} ` : "";
9618
- const more = win.length - 1;
9619
- return `\u26BD ${matchup}live \xB7 syncing\u2026` + (more > 0 ? ` +${more}` : "");
9620
- }
9621
- } else if (!cacheFresh || !liveList.complete) {
9622
- const win = pickAmbientMatch(fixturesInLiveWindow(nowMs, schedule), pick2);
10090
+ const win = syncingWindow(state, nowMs, liveWhole(liveList.complete, state), defaultCompetition, cachedFixtures, schedule, pick2);
10091
+ if (win) {
9623
10092
  const first = win[0];
9624
- if (first) {
9625
- const more = win.length - 1;
9626
- const matchup = isResolvedFixture(first) ? `${teamTok(first.home, flags)} vs ${teamTok(first.away, flags)} ` : "";
9627
- return `\u26BD ${matchup}live \xB7 syncing\u2026` + (more > 0 ? ` +${more}` : "");
9628
- }
10093
+ const matchup = first && isResolvedFixture(first) ? `${teamTok(first.home, flags)} vs ${teamTok(first.away, flags)} ` : "";
10094
+ const more = win.length - 1;
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));
9629
10097
  }
9630
- const next = pickAmbientMatch(upcomingResolved(nowMs, schedule), pick2)[0];
10098
+ const next = countdownFixture(nowMs, schedule, pick2, played ?? playRecordIds(state?.play, nowMs));
9631
10099
  if (next) {
9632
- 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}`);
9633
10104
  }
9634
10105
  if (defaultCompetition && isTournamentWindowOver(nowMs, schedule)) {
9635
- return TOURNAMENT_COMPLETE_LINE;
10106
+ return plain(TOURNAMENT_COMPLETE_LINE);
10107
+ }
10108
+ return plain(NOTHING_KNOWN_LINE);
10109
+ }
10110
+ function lineColumns(opts) {
10111
+ const c = opts.columns;
10112
+ return typeof c === "number" && c >= 1 ? Math.min(Math.floor(c), MAX_LINE_COLUMNS) : MAX_LINE_COLUMNS;
10113
+ }
10114
+ function fitWithMarker(body, marker, width) {
10115
+ const markerWidth = displayWidth(marker);
10116
+ if (markerWidth > width) return truncateVisible(marker, width);
10117
+ const room = width - markerWidth;
10118
+ return (room < 1 ? "" : truncateVisible(body, room)) + marker;
10119
+ }
10120
+ function fitLiveLine(segments, overflow, complete, width) {
10121
+ const markerFor = (dropped) => !complete ? " +more" : overflow + dropped > 0 ? ` +${overflow + dropped}` : "";
10122
+ const bodyOf = (kept2) => "\u26BD " + segments.slice(0, kept2).map((g) => g.text).join(" \xB7 ");
10123
+ let kept = segments.length;
10124
+ while (kept > 1 && displayWidth(bodyOf(kept)) + displayWidth(markerFor(segments.length - kept)) > width) kept--;
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) };
10134
+ }
10135
+ function countdownSchedule(state, defaultCompetition, kind) {
10136
+ const cachedFixtureList = sealFixtures(defaultCompetition ? state?.fixtures : state?.schedule?.fixtures, kind);
10137
+ const cachedFixtures = defaultCompetition ? [...cachedFixtureList.items] : cachedFixtureList.items.filter((m) => hasLiveWindow(m.status));
10138
+ const bundle = defaultCompetition ? allFixtures() : [];
10139
+ const schedule = cachedFixtures.length ? mergeLive(bundle, cachedFixtures) : defaultCompetition ? void 0 : [];
10140
+ return { cachedFixtures, schedule };
10141
+ }
10142
+ function liveWhole(scanComplete, state) {
10143
+ return scanComplete && state?.liveComplete !== false;
10144
+ }
10145
+ function syncingWindow(state, nowMs, liveComplete, defaultCompetition, cachedFixtures, schedule, pick2) {
10146
+ const cacheFresh = !!state && state.degraded !== true && ageMs(state, nowMs) < DISPLAY_STALE_MS;
10147
+ if (cacheFresh && liveComplete) return void 0;
10148
+ if (!defaultCompetition) {
10149
+ if (!scheduleGateOpen(scheduleView(state?.schedule, nowMs), nowMs, { probe: false })) return void 0;
10150
+ return pickAmbientMatch(
10151
+ cachedFixtures.filter((m) => {
10152
+ const k = Date.parse(m.kickoff);
10153
+ return k <= nowMs && nowMs < k + LIVE_WINDOW_MS;
10154
+ }).sort(byKickoff),
10155
+ pick2
10156
+ );
9636
10157
  }
9637
- return "\u26BD \u2014";
10158
+ const win = pickAmbientMatch(fixturesInLiveWindow(nowMs, schedule), pick2);
10159
+ return win.length > 0 ? win : void 0;
10160
+ }
10161
+ function countdownFixture(nowMs, schedule, pick2, played) {
10162
+ return pickAmbientMatch(countdownCandidates(nowMs, schedule, played), pick2)[0];
9638
10163
  }
9639
10164
 
9640
10165
  // src/hook.ts
@@ -9652,8 +10177,12 @@ function rosterPinned(t2, pin) {
9652
10177
  const { team } = lookupTeam(t2.code);
9653
10178
  return team ? { ...t2, name: team.name, flag: team.flag } : t2;
9654
10179
  }
9655
- function line(m, flags, pin) {
9656
- const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
10180
+ function hookNames(m, pin) {
10181
+ return { home: rosterPinned(m.home, pin).name, away: rosterPinned(m.away, pin).name };
10182
+ }
10183
+ function hookLine(m, opts) {
10184
+ const { flags, pin } = opts;
10185
+ const minute = m.status === "FT" ? "FT" : m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
9657
10186
  const h = rosterPinned(m.home, pin);
9658
10187
  const a = rosterPinned(m.away, pin);
9659
10188
  const home = flags ? withFlag(h.name, h.flag, "home") : h.name;
@@ -9670,13 +10199,100 @@ function renderHook(state, opts = {}) {
9670
10199
  const live = pickAmbientMatch(liveList.items, opts.pick);
9671
10200
  const shown = live.slice(0, MAX_HOOK_MATCHES);
9672
10201
  const overflow = live.length - shown.length;
9673
- const lines = shown.map((mm) => line(mm, flags, pin)).join("\n");
10202
+ const lines = shown.map((mm) => hookLine(mm, { flags, pin })).join("\n");
9674
10203
  const more = !liveList.complete ? "\n(more live matches may not be shown)" : overflow > 0 ? `
9675
10204
  (+${overflow} more not shown)` : "";
9676
10205
  return boundContext(`[Claudinho \u2014 live football scores right now]
9677
10206
  ${lines}`, more);
9678
10207
  }
9679
10208
 
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
+ }
10242
+ function ambientView(state, opts = {}) {
10243
+ const now = opts.now ?? /* @__PURE__ */ new Date();
10244
+ const nowMs = now.getTime();
10245
+ const defaultCompetition = opts.defaultCompetition ?? true;
10246
+ const kind = opts.teamKind ?? defaultTeamKind(opts.defaultCompetition);
10247
+ const pin = opts.pin ?? (opts.pick && "team" in opts.pick ? opts.pick.team : void 0);
10248
+ const { lang, ...rest } = opts;
10249
+ const read = { ...rest, now };
10250
+ const hookOpts = { flags: opts.flags ?? true, pin: defaultCompetition };
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
+ });
10256
+ const items = pickAmbientMatch(list.items, opts.pick).map((m) => ({
10257
+ ...m,
10258
+ picked: isPicked(m, opts.pick),
10259
+ pinned: pin !== void 0 && (isPinnedSide(m.home, pin) || isPinnedSide(m.away, pin)),
10260
+ pinnedSide: pinnedSideOf(m, pin),
10261
+ names: hookNames(m, defaultCompetition),
10262
+ line: hookLine(m, hookOpts),
10263
+ lastGoal: lastGoals.get(m) ?? null
10264
+ }));
10265
+ const played = playRecordIds(state?.play, nowMs);
10266
+ const prompt = promptLine(state, { ...read, lang }, played);
10267
+ const context = renderHook(state, read);
10268
+ const { cachedFixtures, schedule } = countdownSchedule(state, defaultCompetition, kind);
10269
+ const age = stampAgeMs(state?.updatedAt, nowMs);
10270
+ const stamp = state && Number.isFinite(age) ? Date.parse(state.updatedAt) : void 0;
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) }));
10278
+ const syncing = list.items.length === 0 && syncingWindow(state, nowMs, liveWhole(list.complete, state), defaultCompetition, cachedFixtures, schedule, opts.pick) !== void 0;
10279
+ return {
10280
+ line: prompt.line,
10281
+ lineParts: prompt.parts,
10282
+ idle: prompt.unfitted === TOURNAMENT_COMPLETE_LINE,
10283
+ empty: prompt.unfitted === NOTHING_KNOWN_LINE,
10284
+ context: context === "" ? null : context,
10285
+ live: { items, total: list.total, shown: list.shown, truncated: list.truncated, complete: list.complete },
10286
+ finished,
10287
+ current: stamp !== void 0 && age < DISPLAY_STALE_MS && state?.degraded !== true && state?.liveComplete === true && !syncing,
10288
+ next: countdownFixture(nowMs, schedule, opts.pick, played) ?? null,
10289
+ degraded: state?.degraded === true,
10290
+ source: typeof state?.source === "string" ? state.source : null,
10291
+ updatedAt: stamp === void 0 ? null : new Date(stamp).toISOString(),
10292
+ staleAfter: staleAt === void 0 || Number.isNaN(staleAt.getTime()) ? null : staleAt.toISOString()
10293
+ };
10294
+ }
10295
+
9680
10296
  // src/refresh.ts
9681
10297
  import { spawn } from "child_process";
9682
10298
  var MIN_REFRESH_MS = 12e3;
@@ -9701,6 +10317,16 @@ function inKnockoutPhase(nowMs, competition) {
9701
10317
  return !!next && isKnockoutStage(next.stage);
9702
10318
  }
9703
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
+ }
9704
10330
  function liveAdapter(source, competition, now) {
9705
10331
  return makeAdapter(source, { competition, enrichGroups: false, now });
9706
10332
  }
@@ -9721,10 +10347,14 @@ async function runRefresh(opts) {
9721
10347
  {
9722
10348
  updatedAt: now.toISOString(),
9723
10349
  live: [],
10350
+ liveComplete: false,
10351
+ // nothing was read
9724
10352
  degraded: true,
9725
10353
  // no live provider served this scope — never claim otherwise
9726
10354
  source,
9727
- competition
10355
+ competition,
10356
+ play: { seen: [], finished: [] }
10357
+ // nothing was read, and no snapshot to carry one from
9728
10358
  },
9729
10359
  idle
9730
10360
  );
@@ -9754,10 +10384,14 @@ async function runRefresh(opts) {
9754
10384
  {
9755
10385
  updatedAt: now.toISOString(),
9756
10386
  live: [],
10387
+ liveComplete: false,
10388
+ // nothing was read
9757
10389
  degraded: until !== void 0,
9758
10390
  source,
9759
10391
  competition,
9760
- ...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
9761
10395
  },
9762
10396
  idle,
9763
10397
  nowMs
@@ -9796,8 +10430,11 @@ async function runRefresh(opts) {
9796
10430
  const { needLive, needFixtures } = plan(base);
9797
10431
  if (!needLive && !needFixtures) return;
9798
10432
  let live = base?.live ?? [];
10433
+ let liveComplete = base ? base.liveComplete : false;
9799
10434
  let degraded = base?.degraded ?? false;
9800
10435
  let updatedAt = base?.updatedAt ?? now.toISOString();
10436
+ const kind = teamKind(competition);
10437
+ let play = carriedPlay(base, now, kind);
9801
10438
  let fixtures = base?.fixtures;
9802
10439
  let fixturesUpdatedAt = base?.fixturesUpdatedAt;
9803
10440
  let fixturesAttemptedAt = base?.fixturesAttemptedAt;
@@ -9808,11 +10445,14 @@ async function runRefresh(opts) {
9808
10445
  const adapter = liveAdapter(source, competition, clock);
9809
10446
  if (needLive) {
9810
10447
  try {
9811
- const r = await getLiveMatches(adapter, now);
9812
- live = r.matches;
10448
+ const r = await getLiveRead(adapter, now, play.seen.map((e) => e.id));
10449
+ live = liveWithLastGoal(r.matches);
10450
+ liveComplete = r.complete === true;
9813
10451
  degraded = r.degraded;
10452
+ play = playAfterRead(play, r, now, kind);
9814
10453
  if (!r.degraded) season = r.season;
9815
10454
  } catch {
10455
+ liveComplete = false;
9816
10456
  degraded = true;
9817
10457
  }
9818
10458
  updatedAt = now.toISOString();
@@ -9847,6 +10487,7 @@ async function runRefresh(opts) {
9847
10487
  {
9848
10488
  updatedAt,
9849
10489
  live,
10490
+ ...liveComplete !== void 0 ? { liveComplete } : {},
9850
10491
  degraded,
9851
10492
  source,
9852
10493
  competition,
@@ -9856,7 +10497,8 @@ async function runRefresh(opts) {
9856
10497
  ...backoffUntil ? { backoffUntil } : {},
9857
10498
  ...season ? { season } : {},
9858
10499
  // Stored with the slice it describes, and only while that slice exists.
9859
- ...fixtures && fixturesSeason ? { fixturesSeason } : {}
10500
+ ...fixtures && fixturesSeason ? { fixturesSeason } : {},
10501
+ play
9860
10502
  },
9861
10503
  token,
9862
10504
  // The cycle's clock, not the carried live stamp: the throttle a
@@ -9935,8 +10577,11 @@ async function refreshOffBundle(c) {
9935
10577
  const clock = () => nowMs + (Date.now() - realStart);
9936
10578
  const adapter = liveAdapter(source, competition, clock);
9937
10579
  let live = base?.live ?? [];
10580
+ let liveComplete = base ? base.liveComplete : false;
9938
10581
  let degraded = base?.degraded ?? false;
9939
10582
  let updatedAt = base?.updatedAt ?? NEVER;
10583
+ const kind = teamKind(competition);
10584
+ let play = carriedPlay(base, now, kind);
9940
10585
  let season = sealSeason(base?.season);
9941
10586
  let index = view.index;
9942
10587
  let display = [...sealFixtures(base?.schedule?.fixtures, teamKind(competition)).items];
@@ -9951,6 +10596,7 @@ async function refreshOffBundle(c) {
9951
10596
  const snapshot = (backoffUntil2) => ({
9952
10597
  updatedAt,
9953
10598
  live,
10599
+ ...liveComplete !== void 0 ? { liveComplete } : {},
9954
10600
  degraded,
9955
10601
  source,
9956
10602
  competition,
@@ -9965,7 +10611,8 @@ async function refreshOffBundle(c) {
9965
10611
  ...scheduleSeason ? { season: scheduleSeason } : {},
9966
10612
  ...inPlayUntil !== void 0 ? { inPlayUntil: new Date(inPlayUntil).toISOString() } : {},
9967
10613
  ...probe ? { probe: true } : {}
9968
- }
10614
+ },
10615
+ play
9969
10616
  });
9970
10617
  if (needDiscovery) {
9971
10618
  const before = { attemptedAt, failures };
@@ -10004,13 +10651,16 @@ async function refreshOffBundle(c) {
10004
10651
  if (ageMs(base, nowMs) >= MIN_REFRESH_MS) {
10005
10652
  const liveAt = clock();
10006
10653
  try {
10007
- const r = await getLiveRead(adapter, now);
10008
- live = r.matches;
10654
+ const r = await getLiveRead(adapter, now, play.seen.map((e) => e.id));
10655
+ live = liveWithLastGoal(r.matches);
10656
+ liveComplete = r.complete === true;
10009
10657
  degraded = r.degraded;
10658
+ play = playAfterRead(play, r, new Date(liveAt), kind);
10010
10659
  if (!r.degraded) season = r.season;
10011
10660
  if (r.matches.length > 0) inPlayUntil = raiseInPlay(inPlayUntil, r.matches, nowMs);
10012
10661
  else if (!r.degraded && r.complete) inPlayUntil = void 0;
10013
10662
  } catch {
10663
+ liveComplete = false;
10014
10664
  degraded = true;
10015
10665
  }
10016
10666
  updatedAt = new Date(liveAt).toISOString();
@@ -10127,6 +10777,9 @@ function manual(path, snippet, why) {
10127
10777
  ${snippet}` };
10128
10778
  }
10129
10779
  function readSettings(path, snippet) {
10780
+ return readSettingsOr(path, (why) => manual(path, snippet, why));
10781
+ }
10782
+ function readSettingsOr(path, refuse) {
10130
10783
  let entry;
10131
10784
  try {
10132
10785
  entry = lstatSync3(path);
@@ -10134,15 +10787,15 @@ function readSettings(path, snippet) {
10134
10787
  return {};
10135
10788
  }
10136
10789
  if (entry.isSymbolicLink() && !existsSync(path)) {
10137
- return manual(path, snippet, "Settings path is a symlink to a missing file:");
10790
+ return refuse("Settings path is a symlink to a missing file:");
10138
10791
  }
10139
10792
  let parsed;
10140
10793
  try {
10141
10794
  parsed = JSON.parse(readFileSync2(path, "utf8"));
10142
10795
  } catch {
10143
- return manual(path, snippet, "Could not parse");
10796
+ return refuse("Could not parse");
10144
10797
  }
10145
- if (!isSettingsObject(parsed)) return manual(path, snippet, "Not a JSON settings object:");
10798
+ if (!isSettingsObject(parsed)) return refuse("Not a JSON settings object:");
10146
10799
  return parsed;
10147
10800
  }
10148
10801
  function isInitResult(v) {
@@ -10233,6 +10886,86 @@ function initHook(opts = {}) {
10233
10886
  message: `Live-score hook configured in ${path}. Restart Claude Code; during matches, the score is injected into context on each prompt.`
10234
10887
  };
10235
10888
  }
10889
+ var PLUGIN_INSTALL_LINE = "/plugin install claudinho --marketplace arturogarrido/claudinho";
10890
+ var CLAUDE_MCP_ONELINER = "claude mcp add claudinho -- npx -y @claudinho/mcp";
10891
+ var PLUGIN_NEXT = `Install the plugin (in Claude Code), and the MCP tools (in a shell):
10892
+ ${PLUGIN_INSTALL_LINE}
10893
+ ${CLAUDE_MCP_ONELINER}`;
10894
+ var RUNS_CLAUDINHO = /claudinho/i;
10895
+ function initPlugin(opts = {}) {
10896
+ const path = opts.path ?? claudeSettingsPath();
10897
+ const refuse = (why) => ({
10898
+ action: "manual",
10899
+ path,
10900
+ message: `${why} ${path}. Nothing was changed: remove the "statusLine" whose command is "${DEFAULT_PROMPT_COMMAND}" and the "hooks.${CLAUDE_HOOK_EVENT}" command "${HOOK_COMMAND}" yourself, if they are there.
10901
+ ${PLUGIN_NEXT}`
10902
+ });
10903
+ const parsed = readSettingsOr(path, refuse);
10904
+ if (isInitResult(parsed)) return parsed;
10905
+ const settings = parsed;
10906
+ if (settings.hooks !== void 0 && !isSettingsObject(settings.hooks)) {
10907
+ return refuse('Unexpected "hooks" shape in');
10908
+ }
10909
+ const hooks = settings.hooks;
10910
+ const eventSlot = hooks?.[CLAUDE_HOOK_EVENT];
10911
+ if (eventSlot !== void 0 && (!Array.isArray(eventSlot) || !validHookMatchers(eventSlot))) {
10912
+ return refuse(`Unexpected "hooks.${CLAUDE_HOOK_EVENT}" shape in`);
10913
+ }
10914
+ const left = /* @__PURE__ */ new Set();
10915
+ const removed = [];
10916
+ const statusLine = settings.statusLine;
10917
+ if (isSettingsObject(statusLine) && typeof statusLine.command === "string") {
10918
+ if (statusLine.command === DEFAULT_PROMPT_COMMAND) {
10919
+ delete settings.statusLine;
10920
+ removed.push("the statusline");
10921
+ } else if (RUNS_CLAUDINHO.test(statusLine.command)) {
10922
+ left.add(statusLine.command);
10923
+ }
10924
+ }
10925
+ if (hooks && Array.isArray(eventSlot)) {
10926
+ let dropped = 0;
10927
+ const matchers = [];
10928
+ for (const matcher of eventSlot) {
10929
+ const commands = matcher.hooks;
10930
+ if (!commands) {
10931
+ matchers.push(matcher);
10932
+ continue;
10933
+ }
10934
+ const kept = commands.filter((h) => h.command !== HOOK_COMMAND);
10935
+ for (const h of kept) if (typeof h.command === "string" && RUNS_CLAUDINHO.test(h.command)) left.add(h.command);
10936
+ dropped += commands.length - kept.length;
10937
+ if (kept.length === commands.length) matchers.push(matcher);
10938
+ else if (kept.length > 0) matchers.push({ ...matcher, hooks: kept });
10939
+ }
10940
+ if (dropped > 0) {
10941
+ removed.push("the live-score hook");
10942
+ if (matchers.length > 0) hooks[CLAUDE_HOOK_EVENT] = matchers;
10943
+ else {
10944
+ delete hooks[CLAUDE_HOOK_EVENT];
10945
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
10946
+ }
10947
+ }
10948
+ }
10949
+ const named = left.size ? `
10950
+ Left in place (not the commands init claude writes; remove them yourself if the plugin replaces them):
10951
+ ${[...left].map((c) => ` ${c}`).join("\n")}` : "";
10952
+ if (removed.length === 0) {
10953
+ return {
10954
+ action: "already",
10955
+ path,
10956
+ message: `Nothing init claude wrote is in ${path}; nothing was changed.${named}
10957
+ ${PLUGIN_NEXT}`
10958
+ };
10959
+ }
10960
+ backupOnce(path);
10961
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n", SETTINGS_WRITE);
10962
+ return {
10963
+ action: "written",
10964
+ path,
10965
+ message: `Removed ${removed.join(" and ")} that init claude wrote from ${path}.${named}
10966
+ ${PLUGIN_NEXT}`
10967
+ };
10968
+ }
10236
10969
 
10237
10970
  // src/providerBackoff.ts
10238
10971
  function persistBackoff(source, competition, until, nowMs) {
@@ -10307,7 +11040,8 @@ function adapterServing({ cfg, adapter, now }, competition) {
10307
11040
  }
10308
11041
  var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
10309
11042
  var MARKETS_CMD_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
10310
- async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeMarketProvider) {
11043
+ async function marketSignalsFor(ctx, matches, baseOpts, now, providerFactory = makeMarketProvider) {
11044
+ const opts = { ...baseOpts, now };
10311
11045
  if (ctx.marketProvider) {
10312
11046
  const b = await getMarketSignals(ctx.marketProvider, matches, opts);
10313
11047
  return { signals: resolvedValues(b), complete: b.complete };
@@ -10318,7 +11052,12 @@ async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeM
10318
11052
  return { signals: resolvedValues(b), complete: b.complete };
10319
11053
  }
10320
11054
  const competition = ctx.cfg.competition;
10321
- 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
+ );
10322
11061
  const result = /* @__PURE__ */ new Map();
10323
11062
  const miss = [];
10324
11063
  for (const m of matches) {
@@ -10340,12 +11079,11 @@ async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeM
10340
11079
  }
10341
11080
  return { signals: result, complete };
10342
11081
  }
10343
- async function reliableMarketSignals(ctx, matches) {
11082
+ async function reliableMarketSignals(ctx, matches, now) {
10344
11083
  if (ctx.cfg.markets === false) return { signals: /* @__PURE__ */ new Map(), complete: true };
10345
- const now = ctx.now ?? /* @__PURE__ */ new Date();
10346
11084
  const relevant = matches.filter((m) => marketRelevant(m, now));
10347
11085
  if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
10348
- const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS);
11086
+ const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS, now);
10349
11087
  const out2 = /* @__PURE__ */ new Map();
10350
11088
  for (const [id, s] of raw.signals) {
10351
11089
  const m = relevant.find((x) => x.id === id);
@@ -10353,18 +11091,21 @@ async function reliableMarketSignals(ctx, matches) {
10353
11091
  }
10354
11092
  return { signals: out2, complete: raw.complete };
10355
11093
  }
10356
- async function reliableMarketSignalFor(ctx, match) {
10357
- const result = await reliableMarketSignals(ctx, [match]);
11094
+ async function reliableMarketSignalFor(ctx, match, now) {
11095
+ const result = await reliableMarketSignals(ctx, [match], now);
10358
11096
  return { signal: result.signals.get(match.id), complete: result.complete };
10359
11097
  }
10360
- function out(line2 = "") {
10361
- process.stdout.write(line2 + "\n");
11098
+ function out(line = "") {
11099
+ process.stdout.write(line + "\n");
10362
11100
  }
10363
11101
  function emitJson(data) {
10364
11102
  out(JSON.stringify(data, null, 2));
10365
11103
  }
10366
11104
  function modeOut(cfg, c) {
10367
- 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)}`);
10368
11109
  }
10369
11110
  function competitionKey(cfg) {
10370
11111
  return selectionExtras(cfg.selection);
@@ -10443,13 +11184,16 @@ async function nextAsked(ctx, team, usage) {
10443
11184
  const { cfg, t: t2 } = ctx;
10444
11185
  const asked = teamAsked(team, cfg);
10445
11186
  if (!asked) throw new InputError(usage);
10446
- const now = ctx.now ?? /* @__PURE__ */ new Date();
10447
11187
  if ("pin" in asked) {
10448
11188
  const code2 = bundleApplies(cfg.competition) ? asked.pin.code : asked.pin.name;
10449
- return { code: code2, next: await nextFixtureForPin(adapterFor(ctx), asked.pin, now) };
11189
+ const adapter2 = adapterFor(ctx);
11190
+ const now2 = requestClock(adapter2, ctx.now);
11191
+ return { code: code2, next: await nextFixtureForPin(adapter2, asked.pin, now2), now: now2 };
10450
11192
  }
10451
11193
  const code = teamQuery(asked.query, usage, t2, cfg.competition);
10452
- return { code, next: await getNextFixtureForTeam(adapterFor(ctx), code, now) };
11194
+ const adapter = adapterFor(ctx);
11195
+ const now = requestClock(adapter, ctx.now);
11196
+ return { code, next: await getNextFixtureForTeam(adapter, code, now), now };
10453
11197
  }
10454
11198
  function nextEmptySentence(next, code, label2, cfg, t2) {
10455
11199
  return verdictNotice(next, cfg.lang) ?? nextHorizonSentence(next, code, cfg.lang) ?? nextNoneReadSentence(next, code, cfg.lang) ?? (next.degraded ? t2("live.degraded") : t2("next.none", { team: label2 }));
@@ -10463,22 +11207,34 @@ function resolveEnvTeam(raw, competition) {
10463
11207
  return /^[A-Za-z]{3}$/.test(raw) ? raw.toUpperCase() : void 0;
10464
11208
  }
10465
11209
  function ambientPick(cfg) {
11210
+ return ambientPreference(cfg).pick;
11211
+ }
11212
+ function ambientPreference(cfg) {
10466
11213
  const raw = process.env.CLAUDINHO_TEAM;
10467
11214
  if (raw) {
10468
11215
  const code = resolveEnvTeam(raw, cfg.competition);
10469
- return code ? { code } : void 0;
11216
+ return code ? { pick: { code }, unreadable: false } : { pick: void 0, unreadable: true };
10470
11217
  }
10471
- return cfg.pin ? { team: cfg.pin } : void 0;
11218
+ return { pick: cfg.pin ? { team: cfg.pin } : void 0, unreadable: false };
11219
+ }
11220
+ function promptLineEnv() {
11221
+ const compact = !["0", "false", "no"].includes(
11222
+ (process.env.CLAUDINHO_COMPACT ?? "").toLowerCase()
11223
+ );
11224
+ const maxRaw = Number.parseInt(process.env.CLAUDINHO_MAX ?? "", 10);
11225
+ const max = Number.isFinite(maxRaw) && maxRaw > 0 ? maxRaw : void 0;
11226
+ return { compact, max };
10472
11227
  }
10473
11228
  async function cmdToday(date, ctx) {
10474
11229
  const { cfg, t: t2 } = ctx;
10475
11230
  precheck(cfg, t2, date);
10476
11231
  const adapter = adapterFor(ctx);
10477
- 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);
10478
11234
  const day = await getMatchesForDate(adapter, targetDate, resolveTz(cfg.tz));
10479
11235
  const { matches, degraded, source } = day;
10480
11236
  const todays = fixturesByDate(targetDate, matches, cfg.tz);
10481
- const market = await reliableMarketSignals(ctx, todays);
11237
+ const market = await reliableMarketSignals(ctx, todays, now);
10482
11238
  if (cfg.json) {
10483
11239
  emitJson({
10484
11240
  date: targetDate,
@@ -10530,13 +11286,14 @@ async function cmdToday(date, ctx) {
10530
11286
  const src = attribution.attributed ? dataSource(source, cfg.lang, c) : "";
10531
11287
  if (src) out(src);
10532
11288
  out(disclaimer(t2, c));
10533
- endScoreCommand(ctx);
11289
+ endScoreCommand(ctx, now);
10534
11290
  }
10535
11291
  async function cmdLive(ctx) {
10536
11292
  const { cfg, t: t2 } = ctx;
10537
11293
  precheck(cfg, t2);
10538
11294
  const adapter = adapterFor(ctx);
10539
- const live = await getLiveMatches(adapter, ctx.now ?? /* @__PURE__ */ new Date());
11295
+ const now = requestClock(adapter, ctx.now);
11296
+ const live = await getLiveMatches(adapter, now);
10540
11297
  const { matches, degraded, source } = live;
10541
11298
  if (cfg.json) {
10542
11299
  emitJson({ degraded, source: source ?? null, matches, ...verdictExtras(live), ...competitionKey(cfg) });
@@ -10564,12 +11321,12 @@ async function cmdLive(ctx) {
10564
11321
  const src = dataSource(source, cfg.lang, c);
10565
11322
  if (src) out(src);
10566
11323
  out(disclaimer(t2, c));
10567
- endScoreCommand(ctx);
11324
+ endScoreCommand(ctx, now);
10568
11325
  }
10569
11326
  async function cmdNext(team, ctx) {
10570
11327
  const { cfg, t: t2 } = ctx;
10571
11328
  precheck(cfg, t2);
10572
- const { code, next } = await nextAsked(ctx, team, teamUsage("next", cfg));
11329
+ const { code, next, now } = await nextAsked(ctx, team, teamUsage("next", cfg));
10573
11330
  const { fixture, degraded, source } = next;
10574
11331
  const label2 = next.team?.name ?? next.query ?? code;
10575
11332
  if (cfg.json) {
@@ -10605,7 +11362,7 @@ async function cmdNext(team, ctx) {
10605
11362
  }
10606
11363
  out();
10607
11364
  out(disclaimer(t2, c));
10608
- endScoreCommand(ctx);
11365
+ endScoreCommand(ctx, now);
10609
11366
  return;
10610
11367
  }
10611
11368
  for (const q of qualifiers) out(c.dim(" " + q));
@@ -10615,7 +11372,7 @@ async function cmdNext(team, ctx) {
10615
11372
  out(
10616
11373
  " " + c.dim(
10617
11374
  joinSegments(
10618
- isLive(fixture.status) ? [stage, when] : [stage, when, countdownPhrase(cfg.lang, fixture.kickoff)]
11375
+ isLive(fixture.status) ? [stage, when] : [stage, when, countdownPhrase(cfg.lang, fixture.kickoff, now)]
10619
11376
  )
10620
11377
  )
10621
11378
  );
@@ -10623,7 +11380,7 @@ async function cmdNext(team, ctx) {
10623
11380
  const src = dataSource(source, cfg.lang, c);
10624
11381
  if (src) out(src);
10625
11382
  out(disclaimer(t2, c));
10626
- endScoreCommand(ctx);
11383
+ endScoreCommand(ctx, now);
10627
11384
  }
10628
11385
  function cmdTeam(query, ctx) {
10629
11386
  const { cfg, t: t2 } = ctx;
@@ -10822,11 +11579,7 @@ function cmdPrompt({ cfg, now }, io = {}) {
10822
11579
  out(renderPromptOutput("\u26BD \u2014", payload));
10823
11580
  return;
10824
11581
  }
10825
- const compact = !["0", "false", "no"].includes(
10826
- (process.env.CLAUDINHO_COMPACT ?? "").toLowerCase()
10827
- );
10828
- const maxRaw = Number.parseInt(process.env.CLAUDINHO_MAX ?? "", 10);
10829
- const max = Number.isFinite(maxRaw) && maxRaw > 0 ? maxRaw : void 0;
11582
+ const { compact, max } = promptLineEnv();
10830
11583
  const state = readCurrentState(cfg.source, cfg.competition);
10831
11584
  const scoreLine = renderPrompt(state, {
10832
11585
  // CLAUDINHO_TEAM by code (offline lookup — hot-path safe), else the saved pin.
@@ -10873,6 +11626,79 @@ function cmdHook({ cfg, now }) {
10873
11626
  } catch {
10874
11627
  }
10875
11628
  }
11629
+ var AMBIENT_EMPTY = JSON.stringify({ line: NOTHING_KNOWN_LINE, empty: true });
11630
+ function cmdAmbient({ cfg, now }, opts = {}) {
11631
+ let printed = false;
11632
+ try {
11633
+ if (cfg.selection.kind === "none") {
11634
+ out(
11635
+ JSON.stringify({
11636
+ ...selectionExtras(cfg.selection),
11637
+ ...verdictExtras(selectionVerdict(cfg.selection)),
11638
+ line: FIRST_RUN_LINE
11639
+ })
11640
+ );
11641
+ return;
11642
+ }
11643
+ if (cfg.selection.kind !== "selected") {
11644
+ out(AMBIENT_EMPTY);
11645
+ return;
11646
+ }
11647
+ const { compact, max } = promptLineEnv();
11648
+ const preference = ambientPreference(cfg);
11649
+ const lang = normalizeLang(cfg.lang);
11650
+ const state = readCurrentState(cfg.source, cfg.competition);
11651
+ const read = ambientView(state, {
11652
+ pick: preference.pick,
11653
+ // The SAVED pin, for each record's `pinned` (whatever CLAUDINHO_TEAM picks).
11654
+ pin: cfg.pin,
11655
+ compact,
11656
+ max,
11657
+ flags: flagsEnabled(),
11658
+ defaultCompetition: bundleApplies(cfg.competition),
11659
+ teamKind: teamKind(cfg.competition),
11660
+ now,
11661
+ columns: opts.columns,
11662
+ // The line's own tokens in the CLI's resolved language (the statusline module reads no environment).
11663
+ lang
11664
+ });
11665
+ const pick2 = preference.pick;
11666
+ const view = {
11667
+ line: read.line,
11668
+ lineParts: read.lineParts,
11669
+ idle: read.idle,
11670
+ empty: read.empty,
11671
+ context: read.context,
11672
+ live: read.live,
11673
+ finished: read.finished,
11674
+ current: read.current,
11675
+ next: read.next,
11676
+ pick: pick2 === void 0 ? null : "code" in pick2 ? { code: pick2.code } : pick2.team,
11677
+ ...preference.unreadable ? { pickUnreadable: true } : {},
11678
+ // A selected competition: core's key is present (see `selectionExtras`).
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),
11683
+ degraded: read.degraded,
11684
+ source: read.source,
11685
+ updatedAt: read.updatedAt,
11686
+ staleAfter: read.staleAfter,
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
11692
+ };
11693
+ out(JSON.stringify(view));
11694
+ printed = true;
11695
+ if (refreshWanted(now?.getTime() ?? Date.now(), state, cfg.competition, cfg.source)) {
11696
+ spawnRefresh(cfg.source, cfg.competition);
11697
+ }
11698
+ } catch {
11699
+ if (!printed) out(AMBIENT_EMPTY);
11700
+ }
11701
+ }
10876
11702
  async function cmdRefresh({ cfg }) {
10877
11703
  if (cfg.selection.kind !== "selected") return;
10878
11704
  await runRefresh({ source: cfg.source, competition: cfg.competition });
@@ -10911,7 +11737,6 @@ var CURSOR_MCP_SNIPPET = `{
10911
11737
  "claudinho": { "command": "npx", "args": ["-y", "@claudinho/mcp"] }
10912
11738
  }
10913
11739
  }`;
10914
- var CLAUDE_MCP_ONELINER = "claude mcp add claudinho -- npx -y @claudinho/mcp";
10915
11740
  function cmdInitCursor(opts, { cfg }) {
10916
11741
  if (opts.print) {
10917
11742
  out("# 1) Cursor CLI statusline \u2192 ~/.cursor/cli-config.json");
@@ -10957,13 +11782,21 @@ function cmdInitClaude(opts, { cfg }) {
10957
11782
  printInitChoose(cfg);
10958
11783
  if (statusRes.action === "written" || hookRes.action === "written") printInitStarCta(cfg);
10959
11784
  }
11785
+ function cmdInitPlugin({ cfg }) {
11786
+ printInitResult(initPlugin(), cfg);
11787
+ }
10960
11788
  async function cmdMatch(id, ctx) {
10961
11789
  const { cfg, t: t2 } = ctx;
10962
11790
  precheck(cfg, t2);
10963
- const found = await getMatchById(adapterFor(ctx), id, ctx.now);
11791
+ const adapter = adapterFor(ctx);
11792
+ const now = requestClock(adapter, ctx.now);
11793
+ const found = await getMatchById(adapter, id, now);
10964
11794
  const { match, degraded, source: liveSource } = found;
10965
- const market = match ? await reliableMarketSignalFor(ctx, 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);
10966
11797
  if (cfg.json) {
11798
+ const rows = [...answer.rows];
11799
+ while (rows.length && rowText(rows[rows.length - 1]) === "") rows.pop();
10967
11800
  emitJson({
10968
11801
  degraded,
10969
11802
  match: match ?? null,
@@ -10975,59 +11808,91 @@ async function cmdMatch(id, ctx) {
10975
11808
  // The span a whole read searched for an id it did not hold (a plain field).
10976
11809
  ...found.window ? { window: found.window } : {},
10977
11810
  ...verdictExtras(found),
11811
+ lines: rows.map(rowText),
11812
+ rows,
11813
+ attribution: answer.attribution || null,
11814
+ disclaimer: answer.disclaimer,
10978
11815
  ...competitionKey(cfg)
10979
11816
  });
10980
11817
  return;
10981
11818
  }
10982
11819
  const c = painterFor(cfg);
10983
- const qualifiers = verdictQualifiers(found, cfg.lang);
10984
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);
10985
11855
  if (!match) {
10986
- modeOut(cfg, c);
10987
- for (const q of qualifiers) out(c.dim(" " + q));
10988
- out(
10989
- c.dim(
10990
- " " + (degraded ? t2("live.degraded") : verdictNotice(found, cfg.lang) ?? matchWindowSentence(found, id, cfg.lang) ?? matchNoneReadSentence(found, id, cfg.lang) ?? t2("match.none", { id }))
10991
- )
10992
- );
10993
- out();
10994
- out(disclaimer(t2, c));
10995
- 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 };
10996
11864
  }
10997
- for (const q of qualifiers) out(c.dim(" " + q));
11865
+ for (const q of qualifiers) row("qualifier", { text: " " + q, style: "dim" });
10998
11866
  const attribution = dayAttribution(found, [match], cfg.lang);
10999
- if (attribution.unserved) out(c.dim(" " + attribution.unserved));
11000
- out(header(`${match.home.name} ${scoreline(match)} ${match.away.name}`, c));
11001
- 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);
11002
11872
  const where = joinSegments([stageLabelI18n(cfg.lang, match), matchLocation(match)]);
11003
- if (where) out(" " + c.dim(where));
11004
- out(
11005
- " " + c.dim(
11006
- `${formatKickoff(match.kickoff, { tz: cfg.tz, locale: cfg.lang })} ${statusToken(match, t2, c)}`.trimEnd()
11007
- )
11008
- );
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);
11009
11878
  const flair = matchFlair(match, flairOpts(cfg));
11010
- if (flair.text) out(" " + (flair.rally ? c.green(flair.text) : c.cyan(flair.text)));
11011
- if (match.events?.length) {
11012
- out();
11013
- for (const e of match.events) {
11014
- out(` ${e.minute}' ${e.type} ${e.teamCode}${e.player ? ` \u2014 ${e.player}` : ""}`);
11015
- }
11016
- }
11879
+ if (flair.text) row("flair", { text: " " }, { text: flair.text, style: flair.rally ? "flair" : "quote" });
11017
11880
  if (market.signal) {
11018
- out();
11019
- 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);
11020
11888
  }
11021
11889
  if (!market.complete) {
11022
- out();
11023
- 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" });
11024
11892
  }
11025
- out();
11026
- if (degraded) out(c.dim(" " + t2(found.earlierRecord ? "feed.earlierRecord" : "feed.degraded")));
11027
- const src = dataSource(liveSource, cfg.lang, c);
11028
- if (src) out(src);
11029
- out(disclaimer(t2, c));
11030
- 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 };
11031
11896
  }
11032
11897
  var MARKET_INFO = "Prediction-market data is informational only.";
11033
11898
  function marketHeaderLine(m, cfg) {
@@ -11040,7 +11905,7 @@ function noSignalLine(m, now, competition) {
11040
11905
  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.";
11041
11906
  }
11042
11907
  function printMarketBlock(m, sig, c) {
11043
- for (const line2 of marketBlock(sig, m)) out(" " + c.dim(line2));
11908
+ for (const line of marketBlock(sig, m)) out(" " + c.dim(line));
11044
11909
  }
11045
11910
  async function cmdMarkets(target, team, ctx) {
11046
11911
  const { cfg, t: t2 } = ctx;
@@ -11050,10 +11915,11 @@ async function cmdMarkets(target, team, ctx) {
11050
11915
  const asked = teamAsked(team, cfg);
11051
11916
  if (!asked) throw new InputError(usage);
11052
11917
  const code = "pin" in asked ? asked.pin.code : resolveTeamArg(asked.query, usage, t2, cfg.competition);
11053
- const now2 = ctx.now ?? /* @__PURE__ */ new Date();
11054
- 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);
11055
11921
  const { match: fixture, degraded } = picked;
11056
- 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 };
11057
11923
  const sig = fixture ? market.signals.get(fixture.id) : void 0;
11058
11924
  const shown = market.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
11059
11925
  if (cfg.json) {
@@ -11097,7 +11963,6 @@ async function cmdMarkets(target, team, ctx) {
11097
11963
  }
11098
11964
  if (target && target !== "today" && !isValidDate(target)) {
11099
11965
  precheck(cfg, t2);
11100
- const now2 = ctx.now ?? /* @__PURE__ */ new Date();
11101
11966
  const scope = marketScopeVerdict(cfg.competition, 0);
11102
11967
  const outOfScope = verdictNotice(scope, cfg.lang);
11103
11968
  if (outOfScope !== void 0) {
@@ -11121,9 +11986,11 @@ async function cmdMarkets(target, team, ctx) {
11121
11986
  out(c3.dim(MARKET_INFO));
11122
11987
  return;
11123
11988
  }
11124
- const found = await getMatchById(adapterFor(ctx), target);
11989
+ const adapter2 = adapterFor(ctx);
11990
+ const now2 = requestClock(adapter2, ctx.now);
11991
+ const found = await getMatchById(adapter2, target, now2);
11125
11992
  const { match } = found;
11126
- 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 };
11127
11994
  const sig = match ? market.signals.get(match.id) : void 0;
11128
11995
  const shown = market.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
11129
11996
  if (cfg.json) {
@@ -11159,12 +12026,13 @@ async function cmdMarkets(target, team, ctx) {
11159
12026
  }
11160
12027
  const explicitDate = target && target !== "today" ? target : void 0;
11161
12028
  precheck(cfg, t2, explicitDate);
11162
- const now = ctx.now ?? /* @__PURE__ */ new Date();
12029
+ const adapter = adapterFor(ctx);
12030
+ const now = requestClock(adapter, ctx.now);
11163
12031
  const date = explicitDate ?? localDate(now.toISOString(), cfg.tz);
11164
- const day = await getMatchesForDate(adapterFor(ctx), date, resolveTz(cfg.tz));
12032
+ const day = await getMatchesForDate(adapter, date, resolveTz(cfg.tz));
11165
12033
  const todays = fixturesByDate(date, day.matches, cfg.tz);
11166
12034
  const relevant = todays.filter((m) => marketRelevant(m, now));
11167
- const { signals, complete } = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
12035
+ const { signals, complete } = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS, now);
11168
12036
  const rows = relevant.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
11169
12037
  (r) => !!r.signal && marketDisplayable(r.match, r.signal)
11170
12038
  );
@@ -11213,8 +12081,8 @@ async function cmdMarkets(target, team, ctx) {
11213
12081
  function pickShareStyle(v) {
11214
12082
  return v === "compact" ? "compact" : "social";
11215
12083
  }
11216
- async function reliableShareSignals(ctx, matches) {
11217
- const raw = await reliableMarketSignals(ctx, matches);
12084
+ async function reliableShareSignals(ctx, matches, now) {
12085
+ const raw = await reliableMarketSignals(ctx, matches, now);
11218
12086
  const out2 = /* @__PURE__ */ new Map();
11219
12087
  for (const [id, s] of raw.signals) {
11220
12088
  const m = matches.find((x) => x.id === id);
@@ -11315,7 +12183,8 @@ async function cmdShare(target, team, opts, ctx) {
11315
12183
  const where = { tz: cfg.tz, locale: cfg.lang, competition: cfg.competition };
11316
12184
  if (target === "live") {
11317
12185
  precheck(cfg, t2);
11318
- 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));
11319
12188
  emitMatchCard(ctx, liveShareCard(live, where), { ...baseOptions, includeMarkets: false }, copy);
11320
12189
  return;
11321
12190
  }
@@ -11356,25 +12225,29 @@ async function cmdShare(target, team, opts, ctx) {
11356
12225
  }
11357
12226
  if (target === "next") {
11358
12227
  precheck(cfg, t2);
11359
- const { code, next } = await nextAsked(ctx, team, teamUsage("share next", cfg));
11360
- const market2 = await reliableShareSignals(ctx, 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);
11361
12230
  emitMatchCard(ctx, nextShareCard(next, code, market2, where), baseOptions, copy);
11362
12231
  return;
11363
12232
  }
11364
12233
  if (target && target !== "today" && !isValidDate(target)) {
11365
12234
  precheck(cfg, t2);
11366
- const found = await getMatchById(adapterFor(ctx), target, ctx.now);
11367
- const market2 = await reliableShareSignals(ctx, 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);
11368
12239
  emitMatchCard(ctx, matchShareCard(found, target, market2, where), baseOptions, copy);
11369
12240
  return;
11370
12241
  }
11371
12242
  const explicitDate = target && target !== "today" ? target : void 0;
11372
12243
  precheck(cfg, t2, explicitDate);
11373
- const date = explicitDate ?? localDate((ctx.now ?? /* @__PURE__ */ new Date()).toISOString(), cfg.tz);
11374
- 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));
11375
12248
  const { matches: all, degraded, source } = day;
11376
12249
  const todays = fixturesByDate(date, all, cfg.tz);
11377
- const market = await reliableShareSignals(ctx, todays);
12250
+ const market = await reliableShareSignals(ctx, todays, now);
11378
12251
  emitMatchCard(
11379
12252
  ctx,
11380
12253
  dateShareCard(
@@ -11462,7 +12335,8 @@ async function pinFor(ctx, competition, query) {
11462
12335
  if (!isKnownSource(cfg.source)) {
11463
12336
  throw new InputError(t2("err.source", { source: cfg.source, sources: KNOWN_SOURCES.join(", ") }));
11464
12337
  }
11465
- 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));
11466
12340
  if (next.team) {
11467
12341
  const { id, code, name } = next.team;
11468
12342
  return id !== void 0 ? { id, code, name } : { code, name };
@@ -11655,8 +12529,8 @@ function maybeStarNudge(ctx) {
11655
12529
  out();
11656
12530
  out(c.dim(` \u2B50 Enjoying Claudinho? Star it \u2192 ${REPO_URL} (claudinho star)`));
11657
12531
  }
11658
- function endScoreCommand(ctx) {
11659
- 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());
11660
12534
  if (over) printTournamentSignOff(ctx);
11661
12535
  else maybeStarNudge(ctx);
11662
12536
  }
@@ -11680,27 +12554,28 @@ function printInitStarCta(cfg) {
11680
12554
  function cmdVibe(ctx) {
11681
12555
  const { cfg } = ctx;
11682
12556
  const bundled = cfg.selection.kind === "selected" && bundleApplies(cfg.competition);
11683
- const pool = vibePool(localDate((ctx.now ?? /* @__PURE__ */ new Date()).toISOString(), cfg.tz), bundled);
11684
- const line2 = pool[Math.floor(Math.random() * pool.length)];
12557
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
12558
+ const pool = vibePool(localDate(now.toISOString(), cfg.tz), bundled);
12559
+ const line = pool[Math.floor(Math.random() * pool.length)];
11685
12560
  let liveSeg;
11686
12561
  try {
11687
12562
  if (cfg.selection.kind !== "selected") throw new Error("no competition");
11688
12563
  const state = readCurrentState(cfg.source, cfg.competition);
11689
12564
  liveSeg = vibeLiveSegment(
11690
12565
  // Sealed with the competition's written kind, like the statusline's.
11691
- liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime(), teamKind(cfg.competition)).items,
12566
+ liveMatchesFromCache(state, now.getTime(), teamKind(cfg.competition)).items,
11692
12567
  // The statusline's and the hook's pick (offline).
11693
12568
  ambientPick(cfg)
11694
12569
  );
11695
12570
  } catch {
11696
12571
  }
11697
12572
  if (cfg.json) {
11698
- emitJson({ vibe: line2, tag: "#VibingLaVidaLoca", ...liveSeg ? { live: liveSeg } : {} });
12573
+ emitJson({ vibe: line, tag: "#VibingLaVidaLoca", ...liveSeg ? { live: liveSeg } : {} });
11699
12574
  return;
11700
12575
  }
11701
12576
  const c = painterFor(cfg);
11702
12577
  out();
11703
- out(" \u26BD " + (liveSeg ? `${liveSeg} \u2014 ` : "") + c.bold(line2 ?? ""));
12578
+ out(" \u26BD " + (liveSeg ? `${liveSeg} \u2014 ` : "") + c.bold(line ?? ""));
11704
12579
  out(" " + c.cyan("#VibingLaVidaLoca"));
11705
12580
  out();
11706
12581
  }
@@ -11714,7 +12589,7 @@ function handlePipeError(stream) {
11714
12589
  }
11715
12590
  handlePipeError(process.stdout);
11716
12591
  handlePipeError(process.stderr);
11717
- var VERSION = "0.11.1";
12592
+ var VERSION = "0.12.0";
11718
12593
  var DISCLAIMER2 = `Claudinho is an ${FAN_PROJECT.toLowerCase()}. ${DISCLAIMER}`;
11719
12594
  function ctxFrom(cmd) {
11720
12595
  let root = cmd;
@@ -11813,7 +12688,10 @@ program.command("prompt").description("print a status line (Claude Code / Cursor
11813
12688
  const cursor = await readCursorPayloadBounded();
11814
12689
  cmdPrompt(ctxFrom(cmd), { cursor });
11815
12690
  });
11816
- var init = program.command("init").description("one-step setup for your agent: `init cursor` or `init claude`");
12691
+ program.command("ambient").description("print the statusline and hook as one JSON object (for a program: the Claude Code plugin)").option("--columns <n>", "fit the line to this many display columns (a positive integer)", positiveInteger).action((opts, cmd) => {
12692
+ cmdAmbient(ctxFrom(cmd), { columns: opts.columns });
12693
+ });
12694
+ var init = program.command("init").description("one-step setup for your agent: `init cursor` or `init claude`; `init plugin` for the Claude Code plugin");
11817
12695
  init.command("cursor").description("set up claudinho for the Cursor CLI (statusline + MCP config)").option("--print", "print the config snippets for manual install instead of writing them").action((opts, cmd) => {
11818
12696
  try {
11819
12697
  cmdInitCursor(opts, ctxFrom(cmd));
@@ -11828,6 +12706,13 @@ init.command("claude").description("set up claudinho for Claude Code (statusline
11828
12706
  fail(e);
11829
12707
  }
11830
12708
  });
12709
+ init.command("plugin").description("remove the statusline and hook that init claude wrote, for the Claude Code plugin").action((_opts, cmd) => {
12710
+ try {
12711
+ cmdInitPlugin(ctxFrom(cmd));
12712
+ } catch (e) {
12713
+ fail(e);
12714
+ }
12715
+ });
11831
12716
  program.command("init-statusline").description("configure the Claude Code statusline to use claudinho").option("--print", "print the settings snippet instead of writing it").option("--command <cmd>", "command to run (default: claudinho prompt)").action((opts, cmd) => {
11832
12717
  try {
11833
12718
  cmdInitStatusline(opts, ctxFrom(cmd));