@claudinho/cli 0.11.1 → 0.11.2

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 +22 -2
  2. package/dist/index.js +337 -72
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @claudinho/cli ⚽
2
2
 
3
- **Live football scores, fixtures and standings in your terminal and your Claude Code and Cursor CLI statusline, for the competition you follow: the World Cup, the Premier League, LALIGA, the Champions League and 12 more.** The `claudinho` command: TZ-aware, localized, scriptable, with read-only market signals for the World Cup. No API key, no signup. It ran the 2026 World Cup from the opener to the final.
3
+ **Live football scores, fixtures and standings in your terminal and your Claude Code and Cursor CLI statusline (and, for a program, `claudinho ambient --json`: the statusline and the hook as one JSON object), for the competition you follow: the World Cup, the Premier League, LALIGA, the Champions League and 12 more.** The `claudinho` command: TZ-aware, localized, scriptable, with read-only market signals for the World Cup. No API key, no signup. It ran the 2026 World Cup from the opener to the final.
4
4
 
5
5
  > ⭐ Installing via `npx` or globally? **[Star the repo](https://github.com/arturogarrido/claudinho)**: a fan project runs on stars. (`claudinho star` shows you how anytime.)
6
6
 
@@ -53,9 +53,11 @@ claudinho share [target] # copy-pasteable snippet: today | live | <date> | <i
53
53
  claudinho prompt # one compact status line (for statusline/tmux/Starship)
54
54
  claudinho init cursor # one-step Cursor setup: statusline + MCP paste (--print for snippets)
55
55
  claudinho init claude # one-step Claude Code setup: statusline + hook + MCP one-liner
56
+ claudinho init plugin # remove the statusline and hook init claude wrote, for the Claude Code plugin
56
57
  claudinho init-statusline # (granular) wire just the Claude Code statusline
57
58
  claudinho init-cursor-statusline # (granular) wire just the Cursor CLI statusline
58
59
  claudinho hook # live-score context for a Claude Code hook (silent off-match)
60
+ claudinho ambient --json # the statusline and the hook as one JSON object, for a program (--columns N)
59
61
  claudinho init-hook # (granular) make Claude itself score-aware (UserPromptSubmit)
60
62
  claudinho vibe # a matchday-coder one-liner (#VibingLaVidaLoca)
61
63
  claudinho star # how to support the project (star the repo ⭐)
@@ -109,7 +111,7 @@ CLAUDINHO_COMPETITION=laliga claudinho table # the environment works to
109
111
  | `--tz <zone>` | IANA timezone, e.g. `America/Mexico_City` (also `CLAUDINHO_TZ`; default: system). Kickoff times **and** which day a fixture falls on are computed in this zone: a late-night-UTC match shows on the day you actually watch it. |
110
112
  | `--json` | machine-readable output for scripting |
111
113
  | `--no-color` | disable ANSI color (also honors `NO_COLOR`; auto-off when piped) |
112
- | `-c, --competition <alias\|slug>` | the competition: an alias such as `premier-league` (the sixteen competitions are listed in the [root README](https://github.com/arturogarrido/claudinho#competitions)), or an ESPN slug such as `eng.1` (any other lower-case dotted ESPN slug works too, labelled experimental). Also `CLAUDINHO_COMPETITION` (an alias or a slug), which the statusline and the hook follow, and the choice `claudinho follow <alias>` saves: the flag, then the environment, then the saved choice; with none, a command says so (exit 1; `--json` prints `{ "competition": null, "noCompetition": true }`), the statusline reads `⚽ claudinho follow`, and the hook stays silent. Every answer names its competition on a line after its header (`Premier League · from the command line`; first where an answer has none; `table` once, before its tables) and in `--json` as `competition`; `share` prints no such line: its card's title names the competition and its run cue selects it; `team` names the World Cup roster it reads. An unknown value is refused with the list. |
114
+ | `-c, --competition <alias\|slug>` | the competition: an alias such as `premier-league` (the sixteen competitions are listed in the [root README](https://github.com/arturogarrido/claudinho#competitions)), or an ESPN slug such as `eng.1` (any other lower-case dotted ESPN slug works too, labelled experimental). Also `CLAUDINHO_COMPETITION` (an alias or a slug), which the statusline, the hook and `ambient --json` follow, and the choice `claudinho follow <alias>` saves: the flag, then the environment, then the saved choice; with none, a command says so (exit 1; `--json` prints `{ "competition": null, "noCompetition": true }`), the statusline reads `⚽ claudinho follow` (`ambient --json` prints it as `line` beside `competition: null`), and the hook stays silent. Every answer names its competition on a line after its header (`Premier League · from the command line`; first where an answer has none; `table` once, before its tables) and in `--json` as `competition`; `share` prints no such line: its card's title names the competition and its run cue selects it; `team` names the World Cup roster it reads. An unknown value is refused with the list. |
113
115
  | `--source <name>` | live data provider (advanced; sensible default) |
114
116
  | `--flavor <level>` | commentary flair: `off`, `subtle`, `full` (default: `full`; also `CLAUDINHO_FLAVOR`) |
115
117
  | `--no-markets` | hide prediction-market signals in `today`/`match` (also `CLAUDINHO_MARKETS=off`) |
@@ -271,6 +273,24 @@ Wires `claudinho hook` into Claude Code's `UserPromptSubmit`. During a match,
271
273
  the live score is injected into Claude's context so it can mention it naturally;
272
274
  off-match it's silent (zero added tokens). Restart Claude Code to activate.
273
275
 
276
+ **Moving to the Claude Code plugin?** `claudinho init plugin` removes the statusline
277
+ (`claudinho prompt`) and the live-score hook (`claudinho hook`) that `init claude` wrote to
278
+ `~/.claude/settings.json`, keeps every other setting, then prints the plugin's install line and the
279
+ MCP one-liner; an edited command (a wrapper, `npx -y @claudinho/cli hook`) stays, and it names it.
280
+ The plugin itself (the band above the prompt, the score toasts, the prompt context) installs from the
281
+ repository: see the [Claude Code plugin](https://github.com/arturogarrido/claudinho#claude-code-plugin) section.
282
+
283
+ ### For a program: `claudinho ambient --json`
284
+
285
+ `claudinho ambient --json [--columns N]` prints the statusline and the hook as one JSON object on
286
+ one line, for a program that draws them itself: `line` (what `claudinho prompt` prints, fitted to N
287
+ columns with the `+N` count kept; N a positive integer), `context` (the hook's block, or `null`),
288
+ `live` (the live matches, each marked `picked` and `pinned`), `current` (an empty list means nothing
289
+ is on only when this is true), `next`, `pick`, `competition`, `degraded`, `source`, `updatedAt`,
290
+ `staleAfter` and `disclaimer` (the non-affiliation sentence). Beside `line` the view says what the line is, `idle` (the edition-complete line, however narrow it was fitted) and `empty` (nothing known), read before the fit so a program never reads a pace or a visibility off the text; and each live record carries `line`, the hook's own line for it with its flags and roster names, which a program says and never composes. It reads only the local cache, never
291
+ the network; the command itself exits 0 whatever the cache holds, and any other `--columns` value is
292
+ refused by the option parser, as a wrong option is for every command.
293
+
274
294
  ## Statusline (Cursor CLI)
275
295
 
276
296
  ```bash
package/dist/index.js CHANGED
@@ -762,7 +762,7 @@ function formatTime(iso, opts = {}) {
762
762
  }).format(when);
763
763
  }
764
764
  var COUNTDOWN_NOW = "now";
765
- function countdown(iso, from = /* @__PURE__ */ new Date()) {
765
+ function countdown(iso, from) {
766
766
  const when = parsedDate(iso);
767
767
  if (!when) return UNKNOWN_TIME;
768
768
  const ms = when.getTime() - from.getTime();
@@ -775,7 +775,7 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
775
775
  if (hours > 0) return `${hours}h${mins}m`;
776
776
  return `${mins}m`;
777
777
  }
778
- function countdownPhrase(lang, iso, from = /* @__PURE__ */ new Date()) {
778
+ function countdownPhrase(lang, iso, from) {
779
779
  const left = countdown(iso, from);
780
780
  return left === COUNTDOWN_NOW ? t(lang, "countdown.now") : t(lang, "next.in", { countdown: left });
781
781
  }
@@ -4745,7 +4745,7 @@ function fetchMeta(result) {
4745
4745
  }
4746
4746
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
4747
4747
  var DEFAULT_COMPETITION = BUNDLED_SLUG;
4748
- var USER_AGENT = `claudinho/${"0.11.1"} (+https://github.com/arturogarrido/claudinho)`;
4748
+ var USER_AGENT = `claudinho/${"0.11.2"} (+https://github.com/arturogarrido/claudinho)`;
4749
4749
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
4750
4750
  var DEFAULT_COOLDOWN_MS = 5 * 6e4;
4751
4751
  var MAX_COOLDOWN_MS = 15 * 6e4;
@@ -6237,6 +6237,9 @@ function mergeLive(base, live) {
6237
6237
  function clockOf(adapter) {
6238
6238
  return new Date(adapter.now?.() ?? Date.now());
6239
6239
  }
6240
+ function requestClock(adapter, given) {
6241
+ return given ?? clockOf(adapter);
6242
+ }
6240
6243
  function providerDayOf(adapter, instant) {
6241
6244
  if (Number.isNaN(instant.getTime())) return "";
6242
6245
  return adapter.bucketDay?.(instant) || instant.toISOString().slice(0, 10);
@@ -6411,7 +6414,7 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
6411
6414
  });
6412
6415
  let refreshRead = {};
6413
6416
  if (candidate) {
6414
- const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id);
6417
+ const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id, now);
6415
6418
  refreshRead = partial ? { partial } : {};
6416
6419
  const m = r.degraded ? candidate : r.match ?? candidate;
6417
6420
  if (!isFinished(m.status)) return { ...r, match: m, ...bothReads(windowRead, refreshRead) };
@@ -8730,6 +8733,14 @@ function makeT(lang) {
8730
8733
  };
8731
8734
  }
8732
8735
 
8736
+ // src/options.ts
8737
+ import { InvalidArgumentError } from "commander";
8738
+ function positiveInteger(value) {
8739
+ const n = /^\d+$/.test(value) ? Number.parseInt(value, 10) : 0;
8740
+ if (!(n > 0)) throw new InvalidArgumentError("a positive integer");
8741
+ return n;
8742
+ }
8743
+
8733
8744
  // src/tagline.ts
8734
8745
  var NAMED = ["the World Cup", "the Premier League", "LALIGA", "the Champions League"];
8735
8746
  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.`;
@@ -8969,7 +8980,9 @@ function readState(source, competition) {
8969
8980
  const bytes = readSmallFile(cachePath(source, competition), MAX_STATE_BYTES);
8970
8981
  if (!bytes) return void 0;
8971
8982
  const parsed = JSON.parse(bytes.toString("utf8"));
8972
- return isCacheState(parsed) ? parsed : void 0;
8983
+ if (!isCacheState(parsed)) return void 0;
8984
+ if (parsed.liveComplete !== void 0 && typeof parsed.liveComplete !== "boolean") delete parsed.liveComplete;
8985
+ return parsed;
8973
8986
  } catch {
8974
8987
  return void 0;
8975
8988
  }
@@ -9448,6 +9461,7 @@ function applyDiscovery(prev, answer, now) {
9448
9461
  // src/statusline.ts
9449
9462
  var DISPLAY_STALE_MS = 5 * 6e4;
9450
9463
  var TOURNAMENT_COMPLETE_LINE = "\u26BD World Cup 2026 is complete \xB7 claudinho follow --list";
9464
+ var NOTHING_KNOWN_LINE = "\u26BD \u2014";
9451
9465
  var FIRST_RUN_LINE = "\u26BD claudinho follow";
9452
9466
  function pickAmbientMatch(matches, pick2) {
9453
9467
  if (!pick2) return [...matches];
@@ -9577,7 +9591,11 @@ function liveMatchesFromCache(state, nowMs, kind) {
9577
9591
  var DEFAULT_MAX_SEGMENTS = 8;
9578
9592
  var MAX_LINE_COLUMNS = 200;
9579
9593
  function renderPrompt(state, opts = {}) {
9580
- return truncateVisible(renderPromptLine(state, opts), MAX_LINE_COLUMNS);
9594
+ return promptLine(state, opts).line;
9595
+ }
9596
+ function promptLine(state, opts = {}) {
9597
+ const unfitted = renderPromptLine(state, opts);
9598
+ return { line: truncateVisible(unfitted, lineColumns(opts)), unfitted };
9581
9599
  }
9582
9600
  function renderPromptLine(state, opts = {}) {
9583
9601
  const now = opts.now ?? /* @__PURE__ */ new Date();
@@ -9589,52 +9607,77 @@ function renderPromptLine(state, opts = {}) {
9589
9607
  const kind = opts.teamKind ?? defaultTeamKind(defaultCompetition);
9590
9608
  const liveList = liveMatchesFromCache(state, nowMs, kind);
9591
9609
  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 : [];
9610
+ const { cachedFixtures, schedule } = countdownSchedule(state, defaultCompetition, kind);
9611
+ const width = lineColumns(opts);
9596
9612
  if (live.length > 0) {
9597
9613
  const ordered = pickAmbientMatch(live, pick2);
9598
9614
  const picked = ordered[0] !== void 0 && isPicked(ordered[0], pick2);
9599
9615
  const max = picked ? 1 : opts.max && opts.max > 0 ? Math.min(opts.max, DEFAULT_MAX_SEGMENTS) : DEFAULT_MAX_SEGMENTS;
9600
9616
  const shown = ordered.slice(0, max);
9601
9617
  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;
9618
+ return fitLiveLine(shown.map((m) => matchSegment(m, compact, flags)), overflow, liveList.complete, width);
9605
9619
  }
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);
9620
+ const win = syncingWindow(state, nowMs, liveWhole(liveList.complete, state), defaultCompetition, cachedFixtures, schedule, pick2);
9621
+ if (win) {
9623
9622
  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
- }
9623
+ const matchup = first && isResolvedFixture(first) ? `${teamTok(first.home, flags)} vs ${teamTok(first.away, flags)} ` : "";
9624
+ const more = win.length - 1;
9625
+ return fitWithMarker(`\u26BD ${matchup}live \xB7 syncing\u2026`, more > 0 ? ` +${more}` : "", width);
9629
9626
  }
9630
- const next = pickAmbientMatch(upcomingResolved(nowMs, schedule), pick2)[0];
9627
+ const next = countdownFixture(nowMs, schedule, pick2);
9631
9628
  if (next) {
9632
9629
  return `${teamTok(next.home, flags)} vs ${teamTok(next.away, flags)} in ${countdown(next.kickoff, now)}`;
9633
9630
  }
9634
9631
  if (defaultCompetition && isTournamentWindowOver(nowMs, schedule)) {
9635
9632
  return TOURNAMENT_COMPLETE_LINE;
9636
9633
  }
9637
- return "\u26BD \u2014";
9634
+ return NOTHING_KNOWN_LINE;
9635
+ }
9636
+ function lineColumns(opts) {
9637
+ const c = opts.columns;
9638
+ return typeof c === "number" && c >= 1 ? Math.min(Math.floor(c), MAX_LINE_COLUMNS) : MAX_LINE_COLUMNS;
9639
+ }
9640
+ function fitWithMarker(body, marker, width) {
9641
+ const markerWidth = displayWidth(marker);
9642
+ if (markerWidth > width) return truncateVisible(marker, width);
9643
+ const room = width - markerWidth;
9644
+ return (room < 1 ? "" : truncateVisible(body, room)) + marker;
9645
+ }
9646
+ function fitLiveLine(segments, overflow, complete, width) {
9647
+ const markerFor = (dropped) => !complete ? " +more" : overflow + dropped > 0 ? ` +${overflow + dropped}` : "";
9648
+ const bodyOf = (kept2) => "\u26BD " + segments.slice(0, kept2).join(" \xB7 ");
9649
+ let kept = segments.length;
9650
+ while (kept > 1 && displayWidth(bodyOf(kept)) + displayWidth(markerFor(segments.length - kept)) > width) kept--;
9651
+ return fitWithMarker(bodyOf(kept), markerFor(segments.length - kept), width);
9652
+ }
9653
+ function countdownSchedule(state, defaultCompetition, kind) {
9654
+ const cachedFixtureList = sealFixtures(defaultCompetition ? state?.fixtures : state?.schedule?.fixtures, kind);
9655
+ const cachedFixtures = defaultCompetition ? [...cachedFixtureList.items] : cachedFixtureList.items.filter((m) => hasLiveWindow(m.status));
9656
+ const bundle = defaultCompetition ? allFixtures() : [];
9657
+ const schedule = cachedFixtures.length ? mergeLive(bundle, cachedFixtures) : defaultCompetition ? void 0 : [];
9658
+ return { cachedFixtures, schedule };
9659
+ }
9660
+ function liveWhole(scanComplete, state) {
9661
+ return scanComplete && state?.liveComplete !== false;
9662
+ }
9663
+ function syncingWindow(state, nowMs, liveComplete, defaultCompetition, cachedFixtures, schedule, pick2) {
9664
+ const cacheFresh = !!state && state.degraded !== true && ageMs(state, nowMs) < DISPLAY_STALE_MS;
9665
+ if (cacheFresh && liveComplete) return void 0;
9666
+ if (!defaultCompetition) {
9667
+ if (!scheduleGateOpen(scheduleView(state?.schedule, nowMs), nowMs, { probe: false })) return void 0;
9668
+ return pickAmbientMatch(
9669
+ cachedFixtures.filter((m) => {
9670
+ const k = Date.parse(m.kickoff);
9671
+ return k <= nowMs && nowMs < k + LIVE_WINDOW_MS;
9672
+ }).sort(byKickoff),
9673
+ pick2
9674
+ );
9675
+ }
9676
+ const win = pickAmbientMatch(fixturesInLiveWindow(nowMs, schedule), pick2);
9677
+ return win.length > 0 ? win : void 0;
9678
+ }
9679
+ function countdownFixture(nowMs, schedule, pick2) {
9680
+ return pickAmbientMatch(upcomingResolved(nowMs, schedule), pick2)[0];
9638
9681
  }
9639
9682
 
9640
9683
  // src/hook.ts
@@ -9652,7 +9695,8 @@ function rosterPinned(t2, pin) {
9652
9695
  const { team } = lookupTeam(t2.code);
9653
9696
  return team ? { ...t2, name: team.name, flag: team.flag } : t2;
9654
9697
  }
9655
- function line(m, flags, pin) {
9698
+ function hookLine(m, opts) {
9699
+ const { flags, pin } = opts;
9656
9700
  const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
9657
9701
  const h = rosterPinned(m.home, pin);
9658
9702
  const a = rosterPinned(m.away, pin);
@@ -9670,13 +9714,51 @@ function renderHook(state, opts = {}) {
9670
9714
  const live = pickAmbientMatch(liveList.items, opts.pick);
9671
9715
  const shown = live.slice(0, MAX_HOOK_MATCHES);
9672
9716
  const overflow = live.length - shown.length;
9673
- const lines = shown.map((mm) => line(mm, flags, pin)).join("\n");
9717
+ const lines = shown.map((mm) => hookLine(mm, { flags, pin })).join("\n");
9674
9718
  const more = !liveList.complete ? "\n(more live matches may not be shown)" : overflow > 0 ? `
9675
9719
  (+${overflow} more not shown)` : "";
9676
9720
  return boundContext(`[Claudinho \u2014 live football scores right now]
9677
9721
  ${lines}`, more);
9678
9722
  }
9679
9723
 
9724
+ // src/ambient.ts
9725
+ function ambientView(state, opts = {}) {
9726
+ const now = opts.now ?? /* @__PURE__ */ new Date();
9727
+ const nowMs = now.getTime();
9728
+ const defaultCompetition = opts.defaultCompetition ?? true;
9729
+ const kind = opts.teamKind ?? defaultTeamKind(opts.defaultCompetition);
9730
+ const pin = opts.pin ?? (opts.pick && "team" in opts.pick ? opts.pick.team : void 0);
9731
+ const read = { ...opts, now };
9732
+ const hookOpts = { flags: opts.flags ?? true, pin: defaultCompetition };
9733
+ const list = liveMatchesFromCache(state, nowMs, kind);
9734
+ const items = pickAmbientMatch(list.items, opts.pick).map((m) => ({
9735
+ ...m,
9736
+ picked: isPicked(m, opts.pick),
9737
+ pinned: pin !== void 0 && (isPinnedSide(m.home, pin) || isPinnedSide(m.away, pin)),
9738
+ line: hookLine(m, hookOpts)
9739
+ }));
9740
+ const prompt = promptLine(state, read);
9741
+ const context = renderHook(state, read);
9742
+ const { cachedFixtures, schedule } = countdownSchedule(state, defaultCompetition, kind);
9743
+ const age = stampAgeMs(state?.updatedAt, nowMs);
9744
+ const stamp = state && Number.isFinite(age) ? Date.parse(state.updatedAt) : void 0;
9745
+ const staleAt = stamp === void 0 ? void 0 : new Date(stamp + DISPLAY_STALE_MS);
9746
+ const syncing = list.items.length === 0 && syncingWindow(state, nowMs, liveWhole(list.complete, state), defaultCompetition, cachedFixtures, schedule, opts.pick) !== void 0;
9747
+ return {
9748
+ line: prompt.line,
9749
+ idle: prompt.unfitted === TOURNAMENT_COMPLETE_LINE,
9750
+ empty: prompt.unfitted === NOTHING_KNOWN_LINE,
9751
+ context: context === "" ? null : context,
9752
+ live: { items, total: list.total, shown: list.shown, truncated: list.truncated, complete: list.complete },
9753
+ current: stamp !== void 0 && age < DISPLAY_STALE_MS && state?.degraded !== true && state?.liveComplete === true && !syncing,
9754
+ next: countdownFixture(nowMs, schedule, opts.pick) ?? null,
9755
+ degraded: state?.degraded === true,
9756
+ source: typeof state?.source === "string" ? state.source : null,
9757
+ updatedAt: stamp === void 0 ? null : new Date(stamp).toISOString(),
9758
+ staleAfter: staleAt === void 0 || Number.isNaN(staleAt.getTime()) ? null : staleAt.toISOString()
9759
+ };
9760
+ }
9761
+
9680
9762
  // src/refresh.ts
9681
9763
  import { spawn } from "child_process";
9682
9764
  var MIN_REFRESH_MS = 12e3;
@@ -9721,6 +9803,8 @@ async function runRefresh(opts) {
9721
9803
  {
9722
9804
  updatedAt: now.toISOString(),
9723
9805
  live: [],
9806
+ liveComplete: false,
9807
+ // nothing was read
9724
9808
  degraded: true,
9725
9809
  // no live provider served this scope — never claim otherwise
9726
9810
  source,
@@ -9754,6 +9838,8 @@ async function runRefresh(opts) {
9754
9838
  {
9755
9839
  updatedAt: now.toISOString(),
9756
9840
  live: [],
9841
+ liveComplete: false,
9842
+ // nothing was read
9757
9843
  degraded: until !== void 0,
9758
9844
  source,
9759
9845
  competition,
@@ -9796,6 +9882,7 @@ async function runRefresh(opts) {
9796
9882
  const { needLive, needFixtures } = plan(base);
9797
9883
  if (!needLive && !needFixtures) return;
9798
9884
  let live = base?.live ?? [];
9885
+ let liveComplete = base ? base.liveComplete : false;
9799
9886
  let degraded = base?.degraded ?? false;
9800
9887
  let updatedAt = base?.updatedAt ?? now.toISOString();
9801
9888
  let fixtures = base?.fixtures;
@@ -9808,11 +9895,13 @@ async function runRefresh(opts) {
9808
9895
  const adapter = liveAdapter(source, competition, clock);
9809
9896
  if (needLive) {
9810
9897
  try {
9811
- const r = await getLiveMatches(adapter, now);
9898
+ const r = await getLiveRead(adapter, now);
9812
9899
  live = r.matches;
9900
+ liveComplete = r.complete === true;
9813
9901
  degraded = r.degraded;
9814
9902
  if (!r.degraded) season = r.season;
9815
9903
  } catch {
9904
+ liveComplete = false;
9816
9905
  degraded = true;
9817
9906
  }
9818
9907
  updatedAt = now.toISOString();
@@ -9847,6 +9936,7 @@ async function runRefresh(opts) {
9847
9936
  {
9848
9937
  updatedAt,
9849
9938
  live,
9939
+ ...liveComplete !== void 0 ? { liveComplete } : {},
9850
9940
  degraded,
9851
9941
  source,
9852
9942
  competition,
@@ -9935,6 +10025,7 @@ async function refreshOffBundle(c) {
9935
10025
  const clock = () => nowMs + (Date.now() - realStart);
9936
10026
  const adapter = liveAdapter(source, competition, clock);
9937
10027
  let live = base?.live ?? [];
10028
+ let liveComplete = base ? base.liveComplete : false;
9938
10029
  let degraded = base?.degraded ?? false;
9939
10030
  let updatedAt = base?.updatedAt ?? NEVER;
9940
10031
  let season = sealSeason(base?.season);
@@ -9951,6 +10042,7 @@ async function refreshOffBundle(c) {
9951
10042
  const snapshot = (backoffUntil2) => ({
9952
10043
  updatedAt,
9953
10044
  live,
10045
+ ...liveComplete !== void 0 ? { liveComplete } : {},
9954
10046
  degraded,
9955
10047
  source,
9956
10048
  competition,
@@ -10006,11 +10098,13 @@ async function refreshOffBundle(c) {
10006
10098
  try {
10007
10099
  const r = await getLiveRead(adapter, now);
10008
10100
  live = r.matches;
10101
+ liveComplete = r.complete === true;
10009
10102
  degraded = r.degraded;
10010
10103
  if (!r.degraded) season = r.season;
10011
10104
  if (r.matches.length > 0) inPlayUntil = raiseInPlay(inPlayUntil, r.matches, nowMs);
10012
10105
  else if (!r.degraded && r.complete) inPlayUntil = void 0;
10013
10106
  } catch {
10107
+ liveComplete = false;
10014
10108
  degraded = true;
10015
10109
  }
10016
10110
  updatedAt = new Date(liveAt).toISOString();
@@ -10127,6 +10221,9 @@ function manual(path, snippet, why) {
10127
10221
  ${snippet}` };
10128
10222
  }
10129
10223
  function readSettings(path, snippet) {
10224
+ return readSettingsOr(path, (why) => manual(path, snippet, why));
10225
+ }
10226
+ function readSettingsOr(path, refuse) {
10130
10227
  let entry;
10131
10228
  try {
10132
10229
  entry = lstatSync3(path);
@@ -10134,15 +10231,15 @@ function readSettings(path, snippet) {
10134
10231
  return {};
10135
10232
  }
10136
10233
  if (entry.isSymbolicLink() && !existsSync(path)) {
10137
- return manual(path, snippet, "Settings path is a symlink to a missing file:");
10234
+ return refuse("Settings path is a symlink to a missing file:");
10138
10235
  }
10139
10236
  let parsed;
10140
10237
  try {
10141
10238
  parsed = JSON.parse(readFileSync2(path, "utf8"));
10142
10239
  } catch {
10143
- return manual(path, snippet, "Could not parse");
10240
+ return refuse("Could not parse");
10144
10241
  }
10145
- if (!isSettingsObject(parsed)) return manual(path, snippet, "Not a JSON settings object:");
10242
+ if (!isSettingsObject(parsed)) return refuse("Not a JSON settings object:");
10146
10243
  return parsed;
10147
10244
  }
10148
10245
  function isInitResult(v) {
@@ -10233,6 +10330,86 @@ function initHook(opts = {}) {
10233
10330
  message: `Live-score hook configured in ${path}. Restart Claude Code; during matches, the score is injected into context on each prompt.`
10234
10331
  };
10235
10332
  }
10333
+ var PLUGIN_INSTALL_LINE = "/plugin install claudinho --marketplace arturogarrido/claudinho";
10334
+ var CLAUDE_MCP_ONELINER = "claude mcp add claudinho -- npx -y @claudinho/mcp";
10335
+ var PLUGIN_NEXT = `Install the plugin (in Claude Code), and the MCP tools (in a shell):
10336
+ ${PLUGIN_INSTALL_LINE}
10337
+ ${CLAUDE_MCP_ONELINER}`;
10338
+ var RUNS_CLAUDINHO = /claudinho/i;
10339
+ function initPlugin(opts = {}) {
10340
+ const path = opts.path ?? claudeSettingsPath();
10341
+ const refuse = (why) => ({
10342
+ action: "manual",
10343
+ path,
10344
+ 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.
10345
+ ${PLUGIN_NEXT}`
10346
+ });
10347
+ const parsed = readSettingsOr(path, refuse);
10348
+ if (isInitResult(parsed)) return parsed;
10349
+ const settings = parsed;
10350
+ if (settings.hooks !== void 0 && !isSettingsObject(settings.hooks)) {
10351
+ return refuse('Unexpected "hooks" shape in');
10352
+ }
10353
+ const hooks = settings.hooks;
10354
+ const eventSlot = hooks?.[CLAUDE_HOOK_EVENT];
10355
+ if (eventSlot !== void 0 && (!Array.isArray(eventSlot) || !validHookMatchers(eventSlot))) {
10356
+ return refuse(`Unexpected "hooks.${CLAUDE_HOOK_EVENT}" shape in`);
10357
+ }
10358
+ const left = /* @__PURE__ */ new Set();
10359
+ const removed = [];
10360
+ const statusLine = settings.statusLine;
10361
+ if (isSettingsObject(statusLine) && typeof statusLine.command === "string") {
10362
+ if (statusLine.command === DEFAULT_PROMPT_COMMAND) {
10363
+ delete settings.statusLine;
10364
+ removed.push("the statusline");
10365
+ } else if (RUNS_CLAUDINHO.test(statusLine.command)) {
10366
+ left.add(statusLine.command);
10367
+ }
10368
+ }
10369
+ if (hooks && Array.isArray(eventSlot)) {
10370
+ let dropped = 0;
10371
+ const matchers = [];
10372
+ for (const matcher of eventSlot) {
10373
+ const commands = matcher.hooks;
10374
+ if (!commands) {
10375
+ matchers.push(matcher);
10376
+ continue;
10377
+ }
10378
+ const kept = commands.filter((h) => h.command !== HOOK_COMMAND);
10379
+ for (const h of kept) if (typeof h.command === "string" && RUNS_CLAUDINHO.test(h.command)) left.add(h.command);
10380
+ dropped += commands.length - kept.length;
10381
+ if (kept.length === commands.length) matchers.push(matcher);
10382
+ else if (kept.length > 0) matchers.push({ ...matcher, hooks: kept });
10383
+ }
10384
+ if (dropped > 0) {
10385
+ removed.push("the live-score hook");
10386
+ if (matchers.length > 0) hooks[CLAUDE_HOOK_EVENT] = matchers;
10387
+ else {
10388
+ delete hooks[CLAUDE_HOOK_EVENT];
10389
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
10390
+ }
10391
+ }
10392
+ }
10393
+ const named = left.size ? `
10394
+ Left in place (not the commands init claude writes; remove them yourself if the plugin replaces them):
10395
+ ${[...left].map((c) => ` ${c}`).join("\n")}` : "";
10396
+ if (removed.length === 0) {
10397
+ return {
10398
+ action: "already",
10399
+ path,
10400
+ message: `Nothing init claude wrote is in ${path}; nothing was changed.${named}
10401
+ ${PLUGIN_NEXT}`
10402
+ };
10403
+ }
10404
+ backupOnce(path);
10405
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n", SETTINGS_WRITE);
10406
+ return {
10407
+ action: "written",
10408
+ path,
10409
+ message: `Removed ${removed.join(" and ")} that init claude wrote from ${path}.${named}
10410
+ ${PLUGIN_NEXT}`
10411
+ };
10412
+ }
10236
10413
 
10237
10414
  // src/providerBackoff.ts
10238
10415
  function persistBackoff(source, competition, until, nowMs) {
@@ -10357,8 +10534,8 @@ async function reliableMarketSignalFor(ctx, match) {
10357
10534
  const result = await reliableMarketSignals(ctx, [match]);
10358
10535
  return { signal: result.signals.get(match.id), complete: result.complete };
10359
10536
  }
10360
- function out(line2 = "") {
10361
- process.stdout.write(line2 + "\n");
10537
+ function out(line = "") {
10538
+ process.stdout.write(line + "\n");
10362
10539
  }
10363
10540
  function emitJson(data) {
10364
10541
  out(JSON.stringify(data, null, 2));
@@ -10443,13 +10620,16 @@ async function nextAsked(ctx, team, usage) {
10443
10620
  const { cfg, t: t2 } = ctx;
10444
10621
  const asked = teamAsked(team, cfg);
10445
10622
  if (!asked) throw new InputError(usage);
10446
- const now = ctx.now ?? /* @__PURE__ */ new Date();
10447
10623
  if ("pin" in asked) {
10448
10624
  const code2 = bundleApplies(cfg.competition) ? asked.pin.code : asked.pin.name;
10449
- return { code: code2, next: await nextFixtureForPin(adapterFor(ctx), asked.pin, now) };
10625
+ const adapter2 = adapterFor(ctx);
10626
+ const now2 = requestClock(adapter2, ctx.now);
10627
+ return { code: code2, next: await nextFixtureForPin(adapter2, asked.pin, now2), now: now2 };
10450
10628
  }
10451
10629
  const code = teamQuery(asked.query, usage, t2, cfg.competition);
10452
- return { code, next: await getNextFixtureForTeam(adapterFor(ctx), code, now) };
10630
+ const adapter = adapterFor(ctx);
10631
+ const now = requestClock(adapter, ctx.now);
10632
+ return { code, next: await getNextFixtureForTeam(adapter, code, now), now };
10453
10633
  }
10454
10634
  function nextEmptySentence(next, code, label2, cfg, t2) {
10455
10635
  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,12 +10643,23 @@ function resolveEnvTeam(raw, competition) {
10463
10643
  return /^[A-Za-z]{3}$/.test(raw) ? raw.toUpperCase() : void 0;
10464
10644
  }
10465
10645
  function ambientPick(cfg) {
10646
+ return ambientPreference(cfg).pick;
10647
+ }
10648
+ function ambientPreference(cfg) {
10466
10649
  const raw = process.env.CLAUDINHO_TEAM;
10467
10650
  if (raw) {
10468
10651
  const code = resolveEnvTeam(raw, cfg.competition);
10469
- return code ? { code } : void 0;
10652
+ return code ? { pick: { code }, unreadable: false } : { pick: void 0, unreadable: true };
10470
10653
  }
10471
- return cfg.pin ? { team: cfg.pin } : void 0;
10654
+ return { pick: cfg.pin ? { team: cfg.pin } : void 0, unreadable: false };
10655
+ }
10656
+ function promptLineEnv() {
10657
+ const compact = !["0", "false", "no"].includes(
10658
+ (process.env.CLAUDINHO_COMPACT ?? "").toLowerCase()
10659
+ );
10660
+ const maxRaw = Number.parseInt(process.env.CLAUDINHO_MAX ?? "", 10);
10661
+ const max = Number.isFinite(maxRaw) && maxRaw > 0 ? maxRaw : void 0;
10662
+ return { compact, max };
10472
10663
  }
10473
10664
  async function cmdToday(date, ctx) {
10474
10665
  const { cfg, t: t2 } = ctx;
@@ -10569,7 +10760,7 @@ async function cmdLive(ctx) {
10569
10760
  async function cmdNext(team, ctx) {
10570
10761
  const { cfg, t: t2 } = ctx;
10571
10762
  precheck(cfg, t2);
10572
- const { code, next } = await nextAsked(ctx, team, teamUsage("next", cfg));
10763
+ const { code, next, now } = await nextAsked(ctx, team, teamUsage("next", cfg));
10573
10764
  const { fixture, degraded, source } = next;
10574
10765
  const label2 = next.team?.name ?? next.query ?? code;
10575
10766
  if (cfg.json) {
@@ -10615,7 +10806,7 @@ async function cmdNext(team, ctx) {
10615
10806
  out(
10616
10807
  " " + c.dim(
10617
10808
  joinSegments(
10618
- isLive(fixture.status) ? [stage, when] : [stage, when, countdownPhrase(cfg.lang, fixture.kickoff)]
10809
+ isLive(fixture.status) ? [stage, when] : [stage, when, countdownPhrase(cfg.lang, fixture.kickoff, now)]
10619
10810
  )
10620
10811
  )
10621
10812
  );
@@ -10822,11 +11013,7 @@ function cmdPrompt({ cfg, now }, io = {}) {
10822
11013
  out(renderPromptOutput("\u26BD \u2014", payload));
10823
11014
  return;
10824
11015
  }
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;
11016
+ const { compact, max } = promptLineEnv();
10830
11017
  const state = readCurrentState(cfg.source, cfg.competition);
10831
11018
  const scoreLine = renderPrompt(state, {
10832
11019
  // CLAUDINHO_TEAM by code (offline lookup — hot-path safe), else the saved pin.
@@ -10873,6 +11060,67 @@ function cmdHook({ cfg, now }) {
10873
11060
  } catch {
10874
11061
  }
10875
11062
  }
11063
+ var AMBIENT_EMPTY = JSON.stringify({ line: NOTHING_KNOWN_LINE, empty: true });
11064
+ function cmdAmbient({ cfg, now }, opts = {}) {
11065
+ let printed = false;
11066
+ try {
11067
+ if (cfg.selection.kind === "none") {
11068
+ out(
11069
+ JSON.stringify({
11070
+ ...selectionExtras(cfg.selection),
11071
+ ...verdictExtras(selectionVerdict(cfg.selection)),
11072
+ line: FIRST_RUN_LINE
11073
+ })
11074
+ );
11075
+ return;
11076
+ }
11077
+ if (cfg.selection.kind !== "selected") {
11078
+ out(AMBIENT_EMPTY);
11079
+ return;
11080
+ }
11081
+ const { compact, max } = promptLineEnv();
11082
+ const preference = ambientPreference(cfg);
11083
+ const state = readCurrentState(cfg.source, cfg.competition);
11084
+ const read = ambientView(state, {
11085
+ pick: preference.pick,
11086
+ // The SAVED pin, for each record's `pinned` (whatever CLAUDINHO_TEAM picks).
11087
+ pin: cfg.pin,
11088
+ compact,
11089
+ max,
11090
+ flags: flagsEnabled(),
11091
+ defaultCompetition: bundleApplies(cfg.competition),
11092
+ teamKind: teamKind(cfg.competition),
11093
+ now,
11094
+ columns: opts.columns
11095
+ });
11096
+ const pick2 = preference.pick;
11097
+ const view = {
11098
+ line: read.line,
11099
+ idle: read.idle,
11100
+ empty: read.empty,
11101
+ context: read.context,
11102
+ live: read.live,
11103
+ current: read.current,
11104
+ next: read.next,
11105
+ pick: pick2 === void 0 ? null : "code" in pick2 ? { code: pick2.code } : pick2.team,
11106
+ ...preference.unreadable ? { pickUnreadable: true } : {},
11107
+ // A selected competition: core's key is present (see `selectionExtras`).
11108
+ ...selectionExtras(cfg.selection),
11109
+ degraded: read.degraded,
11110
+ source: read.source,
11111
+ updatedAt: read.updatedAt,
11112
+ staleAfter: read.staleAfter,
11113
+ disclaimer: DISCLAIMER
11114
+ };
11115
+ out(JSON.stringify(view));
11116
+ printed = true;
11117
+ if (refreshWanted(now?.getTime() ?? Date.now(), state, cfg.competition, cfg.source)) {
11118
+ spawnRefresh(cfg.source, cfg.competition);
11119
+ }
11120
+ } catch {
11121
+ if (!printed) out(AMBIENT_EMPTY);
11122
+ }
11123
+ }
10876
11124
  async function cmdRefresh({ cfg }) {
10877
11125
  if (cfg.selection.kind !== "selected") return;
10878
11126
  await runRefresh({ source: cfg.source, competition: cfg.competition });
@@ -10911,7 +11159,6 @@ var CURSOR_MCP_SNIPPET = `{
10911
11159
  "claudinho": { "command": "npx", "args": ["-y", "@claudinho/mcp"] }
10912
11160
  }
10913
11161
  }`;
10914
- var CLAUDE_MCP_ONELINER = "claude mcp add claudinho -- npx -y @claudinho/mcp";
10915
11162
  function cmdInitCursor(opts, { cfg }) {
10916
11163
  if (opts.print) {
10917
11164
  out("# 1) Cursor CLI statusline \u2192 ~/.cursor/cli-config.json");
@@ -10957,12 +11204,17 @@ function cmdInitClaude(opts, { cfg }) {
10957
11204
  printInitChoose(cfg);
10958
11205
  if (statusRes.action === "written" || hookRes.action === "written") printInitStarCta(cfg);
10959
11206
  }
11207
+ function cmdInitPlugin({ cfg }) {
11208
+ printInitResult(initPlugin(), cfg);
11209
+ }
10960
11210
  async function cmdMatch(id, ctx) {
10961
11211
  const { cfg, t: t2 } = ctx;
10962
11212
  precheck(cfg, t2);
10963
- const found = await getMatchById(adapterFor(ctx), id, ctx.now);
11213
+ const adapter = adapterFor(ctx);
11214
+ const now = requestClock(adapter, ctx.now);
11215
+ const found = await getMatchById(adapter, id, now);
10964
11216
  const { match, degraded, source: liveSource } = found;
10965
- const market = match ? await reliableMarketSignalFor(ctx, match) : { signal: void 0, complete: true };
11217
+ const market = match ? await reliableMarketSignalFor({ ...ctx, now }, match) : { signal: void 0, complete: true };
10966
11218
  if (cfg.json) {
10967
11219
  emitJson({
10968
11220
  degraded,
@@ -11040,7 +11292,7 @@ function noSignalLine(m, now, competition) {
11040
11292
  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
11293
  }
11042
11294
  function printMarketBlock(m, sig, c) {
11043
- for (const line2 of marketBlock(sig, m)) out(" " + c.dim(line2));
11295
+ for (const line of marketBlock(sig, m)) out(" " + c.dim(line));
11044
11296
  }
11045
11297
  async function cmdMarkets(target, team, ctx) {
11046
11298
  const { cfg, t: t2 } = ctx;
@@ -11097,7 +11349,6 @@ async function cmdMarkets(target, team, ctx) {
11097
11349
  }
11098
11350
  if (target && target !== "today" && !isValidDate(target)) {
11099
11351
  precheck(cfg, t2);
11100
- const now2 = ctx.now ?? /* @__PURE__ */ new Date();
11101
11352
  const scope = marketScopeVerdict(cfg.competition, 0);
11102
11353
  const outOfScope = verdictNotice(scope, cfg.lang);
11103
11354
  if (outOfScope !== void 0) {
@@ -11121,7 +11372,9 @@ async function cmdMarkets(target, team, ctx) {
11121
11372
  out(c3.dim(MARKET_INFO));
11122
11373
  return;
11123
11374
  }
11124
- const found = await getMatchById(adapterFor(ctx), target);
11375
+ const adapter = adapterFor(ctx);
11376
+ const now2 = requestClock(adapter, ctx.now);
11377
+ const found = await getMatchById(adapter, target, now2);
11125
11378
  const { match } = found;
11126
11379
  const market = match && marketRelevant(match, now2) ? await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
11127
11380
  const sig = match ? market.signals.get(match.id) : void 0;
@@ -11356,15 +11609,17 @@ async function cmdShare(target, team, opts, ctx) {
11356
11609
  }
11357
11610
  if (target === "next") {
11358
11611
  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] : []);
11612
+ const { code, next, now } = await nextAsked(ctx, team, teamUsage("share next", cfg));
11613
+ const market2 = await reliableShareSignals({ ...ctx, now }, next.fixture ? [next.fixture] : []);
11361
11614
  emitMatchCard(ctx, nextShareCard(next, code, market2, where), baseOptions, copy);
11362
11615
  return;
11363
11616
  }
11364
11617
  if (target && target !== "today" && !isValidDate(target)) {
11365
11618
  precheck(cfg, t2);
11366
- const found = await getMatchById(adapterFor(ctx), target, ctx.now);
11367
- const market2 = await reliableShareSignals(ctx, found.match ? [found.match] : []);
11619
+ const adapter = adapterFor(ctx);
11620
+ const now = requestClock(adapter, ctx.now);
11621
+ const found = await getMatchById(adapter, target, now);
11622
+ const market2 = await reliableShareSignals({ ...ctx, now }, found.match ? [found.match] : []);
11368
11623
  emitMatchCard(ctx, matchShareCard(found, target, market2, where), baseOptions, copy);
11369
11624
  return;
11370
11625
  }
@@ -11681,7 +11936,7 @@ function cmdVibe(ctx) {
11681
11936
  const { cfg } = ctx;
11682
11937
  const bundled = cfg.selection.kind === "selected" && bundleApplies(cfg.competition);
11683
11938
  const pool = vibePool(localDate((ctx.now ?? /* @__PURE__ */ new Date()).toISOString(), cfg.tz), bundled);
11684
- const line2 = pool[Math.floor(Math.random() * pool.length)];
11939
+ const line = pool[Math.floor(Math.random() * pool.length)];
11685
11940
  let liveSeg;
11686
11941
  try {
11687
11942
  if (cfg.selection.kind !== "selected") throw new Error("no competition");
@@ -11695,12 +11950,12 @@ function cmdVibe(ctx) {
11695
11950
  } catch {
11696
11951
  }
11697
11952
  if (cfg.json) {
11698
- emitJson({ vibe: line2, tag: "#VibingLaVidaLoca", ...liveSeg ? { live: liveSeg } : {} });
11953
+ emitJson({ vibe: line, tag: "#VibingLaVidaLoca", ...liveSeg ? { live: liveSeg } : {} });
11699
11954
  return;
11700
11955
  }
11701
11956
  const c = painterFor(cfg);
11702
11957
  out();
11703
- out(" \u26BD " + (liveSeg ? `${liveSeg} \u2014 ` : "") + c.bold(line2 ?? ""));
11958
+ out(" \u26BD " + (liveSeg ? `${liveSeg} \u2014 ` : "") + c.bold(line ?? ""));
11704
11959
  out(" " + c.cyan("#VibingLaVidaLoca"));
11705
11960
  out();
11706
11961
  }
@@ -11714,7 +11969,7 @@ function handlePipeError(stream) {
11714
11969
  }
11715
11970
  handlePipeError(process.stdout);
11716
11971
  handlePipeError(process.stderr);
11717
- var VERSION = "0.11.1";
11972
+ var VERSION = "0.11.2";
11718
11973
  var DISCLAIMER2 = `Claudinho is an ${FAN_PROJECT.toLowerCase()}. ${DISCLAIMER}`;
11719
11974
  function ctxFrom(cmd) {
11720
11975
  let root = cmd;
@@ -11813,7 +12068,10 @@ program.command("prompt").description("print a status line (Claude Code / Cursor
11813
12068
  const cursor = await readCursorPayloadBounded();
11814
12069
  cmdPrompt(ctxFrom(cmd), { cursor });
11815
12070
  });
11816
- var init = program.command("init").description("one-step setup for your agent: `init cursor` or `init claude`");
12071
+ 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) => {
12072
+ cmdAmbient(ctxFrom(cmd), { columns: opts.columns });
12073
+ });
12074
+ 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
12075
  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
12076
  try {
11819
12077
  cmdInitCursor(opts, ctxFrom(cmd));
@@ -11828,6 +12086,13 @@ init.command("claude").description("set up claudinho for Claude Code (statusline
11828
12086
  fail(e);
11829
12087
  }
11830
12088
  });
12089
+ init.command("plugin").description("remove the statusline and hook that init claude wrote, for the Claude Code plugin").action((_opts, cmd) => {
12090
+ try {
12091
+ cmdInitPlugin(ctxFrom(cmd));
12092
+ } catch (e) {
12093
+ fail(e);
12094
+ }
12095
+ });
11831
12096
  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
12097
  try {
11833
12098
  cmdInitStatusline(opts, ctxFrom(cmd));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Live football scores, fixtures and standings in your terminal and your Claude Code and Cursor CLI statusline, for the competition you follow: the World Cup, the Premier League, LALIGA, the Champions League and 12 more. No API key. Independent fan project. Not affiliated with FIFA, any confederation, league or club, or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -76,7 +76,7 @@
76
76
  "tsup": "^8.0.0",
77
77
  "typescript": "^5.7.0",
78
78
  "vitest": "^5.0.3",
79
- "@claudinho/core": "0.11.1"
79
+ "@claudinho/core": "0.11.2"
80
80
  },
81
81
  "scripts": {
82
82
  "build": "tsup",