@claudinho/cli 0.8.19 → 0.9.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 (2) hide show
  1. package/dist/index.js +368 -181
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -381,7 +381,8 @@ function normalizeLang(lang) {
381
381
  }
382
382
  function t(lang, key, vars) {
383
383
  const dict = CATALOGS[normalizeLang(lang)];
384
- let s = dict[key] ?? EN[key] ?? key;
384
+ const en = EN;
385
+ let s = dict[key] ?? en[key] ?? key;
385
386
  if (vars) {
386
387
  for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
387
388
  }
@@ -2943,11 +2944,27 @@ function rosterAtZero(matches) {
2943
2944
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2944
2945
  var DEFAULT_COMPETITION = "fifa.world";
2945
2946
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2946
- var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
2947
+ var USER_AGENT = `claudinho/${"0.9.0"} (+https://github.com/arturogarrido/claudinho)`;
2947
2948
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2948
2949
  function competitionBase(slug) {
2949
2950
  return `${ESPN_SOCCER}/${slug}`;
2950
2951
  }
2952
+ var DEFAULT_TIMEOUT_MS = 6e3;
2953
+ var STANDINGS_SHARE_MS = 3e4;
2954
+ var ProviderError = class extends Error {
2955
+ kind;
2956
+ status;
2957
+ constructor(message, kind, status) {
2958
+ super(message);
2959
+ this.name = "ProviderError";
2960
+ this.kind = kind;
2961
+ this.status = status;
2962
+ }
2963
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
2964
+ get throttled() {
2965
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
2966
+ }
2967
+ };
2951
2968
  function mapStatus(st) {
2952
2969
  const name = (st?.type?.name ?? "").toUpperCase();
2953
2970
  const state = st?.type?.state ?? "";
@@ -3089,6 +3106,20 @@ var EspnAdapter = class {
3089
3106
  capabilities = { push: false, latencyHintSec: 45 };
3090
3107
  /** Cached team-code -> group-letter map (built lazily from standings). */
3091
3108
  groupMap;
3109
+ /**
3110
+ * One in-flight/recent standings fetch shared by fetchStandings and
3111
+ * fetchGroupMap, so a command like `bracket` hits the endpoint once instead
3112
+ * of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
3113
+ * fetch clears the slot — a transient failure is never cached.
3114
+ */
3115
+ standingsShared;
3116
+ /**
3117
+ * The most recent request failure (best-effort under concurrency; cleared
3118
+ * when a new request starts). A post-hoc hint for callers whose result path
3119
+ * fails closed to `degraded` booleans but who still need to distinguish a
3120
+ * throttle (persist a backoff) from an ordinary blip.
3121
+ */
3122
+ lastError;
3092
3123
  async fetchByDate(dateISO) {
3093
3124
  return this.fetchScoreboard(toEspnDate(dateISO));
3094
3125
  }
@@ -3110,37 +3141,58 @@ var EspnAdapter = class {
3110
3141
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3111
3142
  return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
3112
3143
  }
3144
+ /** The shared standings fetch (see {@link standingsShared}). */
3145
+ sharedStandings() {
3146
+ const now = Date.now();
3147
+ if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3148
+ return this.standingsShared.promise;
3149
+ }
3150
+ const promise = this.get(this.standingsUrl()).then(
3151
+ (d) => parseStandings(d)
3152
+ );
3153
+ this.standingsShared = { at: now, promise };
3154
+ promise.catch(() => {
3155
+ if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3156
+ });
3157
+ return promise;
3158
+ }
3113
3159
  /**
3114
3160
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3115
3161
  * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3116
3162
  * non-group `children` are filtered out by {@link parseStandings}.
3117
3163
  */
3118
3164
  async fetchStandings() {
3119
- return parseStandings(await this.get(this.standingsUrl()));
3165
+ return this.sharedStandings();
3120
3166
  }
3121
3167
  /**
3122
3168
  * Build (and cache) a team-code -> group-letter map from the standings
3123
- * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
3124
- * same parse as {@link fetchStandings}, so the two never drift.
3169
+ * endpoint. Best-effort: returns {} if standings are unavailable but a
3170
+ * transient failure is NOT cached (only a successful parse pins the map), so
3171
+ * one blip can't silently drop group letters for the adapter's lifetime.
3172
+ * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3173
+ * drift and one command never fetches standings twice.
3125
3174
  */
3126
3175
  async fetchGroupMap(force = false) {
3127
3176
  if (this.groupMap && !force) return this.groupMap;
3128
- const map = {};
3129
3177
  try {
3130
- const tables = parseStandings(await this.get(this.standingsUrl()));
3178
+ const tables = await this.sharedStandings();
3179
+ const map = {};
3131
3180
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3181
+ this.groupMap = map;
3182
+ return map;
3132
3183
  } catch {
3184
+ return {};
3133
3185
  }
3134
- this.groupMap = map;
3135
- return map;
3136
3186
  }
3137
3187
  async fetchScoreboard(dates) {
3138
3188
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3139
3189
  const url = new URL(`${base}/scoreboard`);
3140
3190
  url.searchParams.set("limit", "300");
3141
3191
  if (dates) url.searchParams.set("dates", dates);
3142
- const groupByTeam = this.opts.enrichGroups === false ? {} : await this.fetchGroupMap();
3143
- const data = await this.get(url.toString());
3192
+ const [groupByTeam, data] = await Promise.all([
3193
+ this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3194
+ this.get(url.toString())
3195
+ ]);
3144
3196
  return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3145
3197
  }
3146
3198
  async get(url) {
@@ -3148,24 +3200,51 @@ var EspnAdapter = class {
3148
3200
  const controller = new AbortController();
3149
3201
  const timer = setTimeout(
3150
3202
  () => controller.abort(),
3151
- this.opts.timeoutMs ?? 15e3
3203
+ this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
3152
3204
  );
3205
+ this.lastError = void 0;
3153
3206
  try {
3154
- const res = await doFetch(url, {
3155
- signal: controller.signal,
3156
- // ESPN never legitimately redirects; following one would sidestep the
3157
- // fixed-host guarantee, so treat any redirect as a failure (degraded).
3158
- redirect: "error",
3159
- headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3160
- });
3207
+ let res;
3208
+ try {
3209
+ res = await doFetch(url, {
3210
+ signal: controller.signal,
3211
+ // ESPN never legitimately redirects; following one would sidestep the
3212
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3213
+ redirect: "error",
3214
+ headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3215
+ });
3216
+ } catch (e) {
3217
+ throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3218
+ }
3161
3219
  if (!res.ok) {
3162
- throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3220
+ throw new ProviderError(
3221
+ `ESPN request failed: ${res.status} ${res.statusText}`,
3222
+ "http",
3223
+ res.status
3224
+ );
3163
3225
  }
3164
3226
  const length = Number(res.headers?.get?.("content-length"));
3165
3227
  if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3166
- throw new Error(`ESPN response too large: ${length} bytes`);
3228
+ throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
3229
+ }
3230
+ try {
3231
+ return await res.json();
3232
+ } catch (e) {
3233
+ throw new ProviderError(
3234
+ `ESPN response unparseable: ${e?.message ?? e}`,
3235
+ "parse"
3236
+ );
3237
+ }
3238
+ } catch (e) {
3239
+ const pe = e instanceof ProviderError ? e : new ProviderError(String(e?.message ?? e), "http");
3240
+ this.lastError = pe;
3241
+ if (typeof process !== "undefined" && process.env?.CLAUDINHO_DEBUG) {
3242
+ process.stderr.write(
3243
+ `claudinho: espn ${pe.kind}${pe.status ? ` ${pe.status}` : ""}: ${url}
3244
+ `
3245
+ );
3167
3246
  }
3168
- return await res.json();
3247
+ throw pe;
3169
3248
  } finally {
3170
3249
  clearTimeout(timer);
3171
3250
  }
@@ -3889,13 +3968,18 @@ function resolveCompetition(explicit) {
3889
3968
  }
3890
3969
  return DEFAULT_COMPETITION;
3891
3970
  }
3971
+ var KNOWN_SOURCES = ["espn"];
3892
3972
  function makeAdapter(source = "espn") {
3893
3973
  switch (source) {
3894
- default: {
3974
+ case "espn": {
3895
3975
  const competition = resolveCompetition();
3896
3976
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
3897
3977
  return new EspnAdapter({ baseUrl });
3898
3978
  }
3979
+ default:
3980
+ throw new Error(
3981
+ `Unknown data source "${source}" (available: ${KNOWN_SOURCES.join(", ")})`
3982
+ );
3899
3983
  }
3900
3984
  }
3901
3985
  function mergeLive(base, live) {
@@ -3933,8 +4017,16 @@ async function getStandings(adapter, group) {
3933
4017
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
3934
4018
  return { tables, degraded: true };
3935
4019
  }
3936
- var KNOCKOUT_WINDOW_START = "20260628";
3937
- var KNOCKOUT_WINDOW_END = "20260719";
4020
+ var knockoutWindowMemo;
4021
+ function knockoutWindow() {
4022
+ if (knockoutWindowMemo !== void 0) return knockoutWindowMemo;
4023
+ const days = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY").map((m) => m.kickoff.slice(0, 10).replace(/-/g, "")).filter((d) => d.length === 8);
4024
+ knockoutWindowMemo = days.length ? {
4025
+ start: days.reduce((a, b) => a < b ? a : b),
4026
+ end: days.reduce((a, b) => a > b ? a : b)
4027
+ } : null;
4028
+ return knockoutWindowMemo;
4029
+ }
3938
4030
  async function getBracket(adapter, opts = {}) {
3939
4031
  const topology = loadBracketTopology();
3940
4032
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
@@ -3942,7 +4034,8 @@ async function getBracket(adapter, opts = {}) {
3942
4034
  let liveDegraded = true;
3943
4035
  let source;
3944
4036
  try {
3945
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4037
+ const win = knockoutWindow();
4038
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
3946
4039
  matches = mergeLive(base, live);
3947
4040
  liveDegraded = false;
3948
4041
  source = adapter.name;
@@ -3975,8 +4068,9 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
3975
4068
  let fixtures = allFixtures();
3976
4069
  let overlayFailed = false;
3977
4070
  try {
3978
- if (adapter.fetchWindow) {
3979
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END));
4071
+ const win = knockoutWindow();
4072
+ if (adapter.fetchWindow && win) {
4073
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
3980
4074
  }
3981
4075
  } catch {
3982
4076
  overlayFailed = true;
@@ -3999,7 +4093,8 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
3999
4093
  let degraded = true;
4000
4094
  let liveById;
4001
4095
  try {
4002
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4096
+ const win = knockoutWindow();
4097
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4003
4098
  matches = mergeLive(base, live);
4004
4099
  degraded = false;
4005
4100
  liveById = new Set(live.map((m) => m.id));
@@ -4010,10 +4105,11 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
4010
4105
  return { fixture, degraded, source };
4011
4106
  }
4012
4107
  async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
4013
- if (!adapter.fetchWindow) return { fixtures: [], degraded: true };
4108
+ const win = knockoutWindow();
4109
+ if (!adapter.fetchWindow || !win) return { fixtures: [], degraded: true };
4014
4110
  let live;
4015
4111
  try {
4016
- live = await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END);
4112
+ live = await adapter.fetchWindow(win.start, win.end);
4017
4113
  } catch {
4018
4114
  return { fixtures: [], degraded: true };
4019
4115
  }
@@ -4261,7 +4357,7 @@ var mapping_2026_default = {
4261
4357
  var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4262
4358
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4263
4359
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4264
- var DEFAULT_TIMEOUT_MS = 8e3;
4360
+ var DEFAULT_TIMEOUT_MS2 = 8e3;
4265
4361
  var WC_SERIES_SLUG = "soccer-fifwc";
4266
4362
  var WC_SPORT = "fifwc";
4267
4363
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4299,7 +4395,7 @@ var PolymarketProvider = class {
4299
4395
  const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4300
4396
  const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4301
4397
  if (slugs.length === 0) return { checked: true };
4302
- const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4398
+ const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4303
4399
  try {
4304
4400
  for (const slug of slugs) {
4305
4401
  const remaining = deadline - Date.now();
@@ -4319,7 +4415,7 @@ var PolymarketProvider = class {
4319
4415
  const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
4320
4416
  const doFetch = this.opts.fetchImpl ?? fetch;
4321
4417
  const res = await doFetch(url, {
4322
- signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4418
+ signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
4323
4419
  // Gamma never legitimately redirects; following one would sidestep the
4324
4420
  // host allow-list (it only validates the base URL), so reject redirects.
4325
4421
  redirect: "error",
@@ -4810,6 +4906,82 @@ function resolveConfig(opts) {
4810
4906
  };
4811
4907
  }
4812
4908
 
4909
+ // src/cursorPayload.ts
4910
+ import { readFileSync } from "fs";
4911
+ function parseCursorPayload(raw) {
4912
+ try {
4913
+ const trimmed = raw.trim();
4914
+ if (!trimmed) return void 0;
4915
+ return JSON.parse(trimmed);
4916
+ } catch {
4917
+ return void 0;
4918
+ }
4919
+ }
4920
+ function readCursorPayload() {
4921
+ try {
4922
+ if (process.stdin.isTTY) return void 0;
4923
+ return parseCursorPayload(readFileSync(0, "utf8"));
4924
+ } catch {
4925
+ return void 0;
4926
+ }
4927
+ }
4928
+ function readCursorPayloadBounded(timeoutMs = 100, stdin = process.stdin) {
4929
+ if (stdin.isTTY) return Promise.resolve(void 0);
4930
+ return new Promise((resolve) => {
4931
+ const chunks = [];
4932
+ const done = () => {
4933
+ clearTimeout(timer);
4934
+ stdin.off("data", onData);
4935
+ stdin.off("end", done);
4936
+ stdin.off("error", done);
4937
+ stdin.pause();
4938
+ resolve(parseCursorPayload(Buffer.concat(chunks).toString("utf8")));
4939
+ };
4940
+ const onData = (c) => {
4941
+ chunks.push(c);
4942
+ };
4943
+ const timer = setTimeout(done, timeoutMs);
4944
+ timer.unref?.();
4945
+ stdin.on("data", onData);
4946
+ stdin.once("end", done);
4947
+ stdin.once("error", done);
4948
+ stdin.resume();
4949
+ });
4950
+ }
4951
+ function looksLikeCursorPayload(payload) {
4952
+ return !!(payload.model?.display_name || payload.context_window != null || payload.render_width_chars != null || payload.worktree?.name || payload.vim?.mode);
4953
+ }
4954
+ function cursorMetaEnabled(payload) {
4955
+ const v = (process.env.CLAUDINHO_CURSOR_META ?? "").toLowerCase();
4956
+ if (v === "0" || v === "false" || v === "no") return false;
4957
+ if (v === "1" || v === "true" || v === "yes") return !!payload;
4958
+ if (v === "auto") return !!payload && looksLikeCursorPayload(payload);
4959
+ return false;
4960
+ }
4961
+ function renderCursorMetaLine(payload) {
4962
+ const parts = [];
4963
+ const model = payload.model?.display_name;
4964
+ if (model) {
4965
+ let label = model;
4966
+ if (payload.model?.param_summary) label += ` ${payload.model.param_summary}`;
4967
+ parts.push(label);
4968
+ }
4969
+ const pct2 = payload.context_window?.used_percentage;
4970
+ if (typeof pct2 === "number" && Number.isFinite(pct2)) {
4971
+ parts.push(`ctx ${Math.floor(pct2)}%`);
4972
+ }
4973
+ if (payload.worktree?.name) parts.push(`wt ${payload.worktree.name}`);
4974
+ if (payload.vim?.mode) parts.push(payload.vim.mode);
4975
+ if (parts.length === 0) return void 0;
4976
+ return `\x1B[90m${parts.join(" ")}\x1B[0m`;
4977
+ }
4978
+ function renderPromptOutput(scoreLine, payload) {
4979
+ const meta = payload && cursorMetaEnabled(payload) ? renderCursorMetaLine(payload) : void 0;
4980
+ if (meta) return `${scoreLine}
4981
+ ${meta}`;
4982
+ return scoreLine;
4983
+ }
4984
+
4813
4985
  // src/i18n.ts
4814
4986
  var EN2 = {
4815
4987
  "today.title": "Today's matches",
@@ -4844,6 +5016,7 @@ var EN2 = {
4844
5016
  "col.gd": "GD",
4845
5017
  "col.pts": "Pts",
4846
5018
  "err.date": "Invalid date {date}. Use YYYY-MM-DD.",
5019
+ "err.source": "Unknown data source {source}. Available: {sources}.",
4847
5020
  "warn.tz": "Unknown timezone {tz}; using system timezone.",
4848
5021
  "warn.lang": "Unsupported language {lang}; using English. (supported: en, es, pt, fr)",
4849
5022
  disclaimer: "Not affiliated with FIFA or Anthropic."
@@ -4881,6 +5054,7 @@ var ES2 = {
4881
5054
  "col.gd": "DG",
4882
5055
  "col.pts": "Pts",
4883
5056
  "err.date": "Fecha inv\xE1lida {date}. Usa AAAA-MM-DD.",
5057
+ "err.source": "Fuente de datos desconocida {source}. Disponibles: {sources}.",
4884
5058
  "warn.tz": "Zona horaria desconocida {tz}; usando la del sistema.",
4885
5059
  "warn.lang": "Idioma no soportado {lang}; usando ingl\xE9s. (disponibles: en, es, pt, fr)",
4886
5060
  disclaimer: "No afiliado a FIFA ni Anthropic."
@@ -4918,6 +5092,7 @@ var PT2 = {
4918
5092
  "col.gd": "SG",
4919
5093
  "col.pts": "Pts",
4920
5094
  "err.date": "Data inv\xE1lida {date}. Use AAAA-MM-DD.",
5095
+ "err.source": "Fonte de dados desconhecida {source}. Dispon\xEDveis: {sources}.",
4921
5096
  "warn.tz": "Fuso hor\xE1rio desconhecido {tz}; usando o do sistema.",
4922
5097
  "warn.lang": "Idioma n\xE3o suportado {lang}; usando ingl\xEAs. (dispon\xEDveis: en, es, pt, fr)",
4923
5098
  disclaimer: "N\xE3o afiliado \xE0 FIFA nem \xE0 Anthropic."
@@ -4955,6 +5130,7 @@ var FR2 = {
4955
5130
  "col.gd": "Diff",
4956
5131
  "col.pts": "Pts",
4957
5132
  "err.date": "Date invalide {date}. Utilisez AAAA-MM-JJ.",
5133
+ "err.source": "Source de donn\xE9es inconnue {source}. Disponibles : {sources}.",
4958
5134
  "warn.tz": "Fuseau horaire inconnu {tz} ; utilisation du fuseau syst\xE8me.",
4959
5135
  "warn.lang": "Langue non prise en charge {lang} ; utilisation de l\u2019anglais. (disponibles : en, es, pt, fr)",
4960
5136
  disclaimer: "Non affili\xE9 \xE0 la FIFA ni \xE0 Anthropic."
@@ -4962,8 +5138,9 @@ var FR2 = {
4962
5138
  var CATALOGS2 = { en: EN2, es: ES2, pt: PT2, fr: FR2 };
4963
5139
  function makeT(lang) {
4964
5140
  const dict = CATALOGS2[lang] ?? EN2;
5141
+ const en = EN2;
4965
5142
  return (key, vars) => {
4966
- let s = dict[key] ?? EN2[key] ?? key;
5143
+ let s = dict[key] ?? en[key] ?? key;
4967
5144
  if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
4968
5145
  return s;
4969
5146
  };
@@ -5048,21 +5225,38 @@ function dataSource(source, lang, c) {
5048
5225
  }
5049
5226
 
5050
5227
  // src/marketCache.ts
5051
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
5228
+ import { readFileSync as readFileSync2 } from "fs";
5229
+ import { join as join2 } from "path";
5230
+
5231
+ // src/paths.ts
5232
+ import { closeSync, mkdirSync, openSync, renameSync, writeSync } from "fs";
5052
5233
  import { homedir } from "os";
5053
- import { join } from "path";
5054
- var POSITIVE_TTL_MS = 10 * 6e4;
5055
- var NEGATIVE_TTL_MS = 3 * 6e4;
5234
+ import { dirname, join } from "path";
5056
5235
  function cacheDir() {
5057
5236
  const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
5058
5237
  return join(base, "claudinho");
5059
5238
  }
5239
+ function writeFileAtomic(path, data) {
5240
+ mkdirSync(dirname(path), { recursive: true });
5241
+ const tmp = `${path}.${process.pid}.tmp`;
5242
+ const fd = openSync(tmp, "w");
5243
+ try {
5244
+ writeSync(fd, data);
5245
+ } finally {
5246
+ closeSync(fd);
5247
+ }
5248
+ renameSync(tmp, path);
5249
+ }
5250
+
5251
+ // src/marketCache.ts
5252
+ var POSITIVE_TTL_MS = 10 * 6e4;
5253
+ var NEGATIVE_TTL_MS = 3 * 6e4;
5060
5254
  function cachePath() {
5061
- return join(cacheDir(), "market-signals.json");
5255
+ return join2(cacheDir(), "market-signals.json");
5062
5256
  }
5063
5257
  function readFile() {
5064
5258
  try {
5065
- return JSON.parse(readFileSync(cachePath(), "utf8"));
5259
+ return JSON.parse(readFileSync2(cachePath(), "utf8"));
5066
5260
  } catch {
5067
5261
  return void 0;
5068
5262
  }
@@ -5093,23 +5287,18 @@ function writeMarketCache(source, competition, attempted, fetched, now = Date.no
5093
5287
  for (const id of attempted) {
5094
5288
  base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
5095
5289
  }
5096
- mkdirSync(cacheDir(), { recursive: true });
5097
- const tmp = join(cacheDir(), `market-signals.${process.pid}.tmp`);
5098
- writeFileSync(tmp, JSON.stringify(base));
5099
- renameSync(tmp, cachePath());
5290
+ writeFileAtomic(cachePath(), JSON.stringify(base));
5100
5291
  } catch {
5101
5292
  }
5102
5293
  }
5103
5294
 
5104
5295
  // src/starNudge.ts
5105
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
5106
- import { homedir as homedir2 } from "os";
5107
- import { dirname, join as join2 } from "path";
5296
+ import { readFileSync as readFileSync3 } from "fs";
5297
+ import { join as join3 } from "path";
5108
5298
  var REPO_URL = "https://github.com/arturogarrido/claudinho";
5109
5299
  var NUDGE_EVERY = 5;
5110
5300
  function counterPath() {
5111
- const base = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
5112
- return join2(base, "claudinho", "runs.json");
5301
+ return join3(cacheDir(), "runs.json");
5113
5302
  }
5114
5303
  function shouldNudge(runCount, every = NUDGE_EVERY) {
5115
5304
  return runCount > 0 && runCount % every === 0;
@@ -5118,14 +5307,13 @@ function bumpRunCount(path = counterPath()) {
5118
5307
  try {
5119
5308
  let count = 0;
5120
5309
  try {
5121
- const raw = JSON.parse(readFileSync2(path, "utf8"));
5310
+ const raw = JSON.parse(readFileSync3(path, "utf8"));
5122
5311
  if (typeof raw.count === "number" && Number.isFinite(raw.count)) count = raw.count;
5123
5312
  } catch {
5124
5313
  count = 0;
5125
5314
  }
5126
5315
  count += 1;
5127
- mkdirSync2(dirname(path), { recursive: true });
5128
- writeFileSync2(path, JSON.stringify({ count }), "utf8");
5316
+ writeFileAtomic(path, JSON.stringify({ count }));
5129
5317
  return count;
5130
5318
  } catch {
5131
5319
  return void 0;
@@ -5160,53 +5348,56 @@ function copyToClipboard(text, platform = process.platform) {
5160
5348
 
5161
5349
  // src/cache.ts
5162
5350
  import {
5163
- closeSync,
5164
- mkdirSync as mkdirSync3,
5165
- openSync,
5166
- readFileSync as readFileSync3,
5167
- renameSync as renameSync2,
5351
+ closeSync as closeSync2,
5352
+ mkdirSync as mkdirSync2,
5353
+ openSync as openSync2,
5354
+ readFileSync as readFileSync4,
5168
5355
  rmSync,
5169
5356
  statSync,
5170
- writeSync
5357
+ writeSync as writeSync2
5171
5358
  } from "fs";
5172
- import { homedir as homedir3 } from "os";
5173
- import { join as join3 } from "path";
5359
+ import { join as join4 } from "path";
5360
+ var CACHE_VERSION = 2;
5174
5361
  var LOCK_STALE_MS = 6e4;
5175
- function cacheDir2() {
5176
- const base = process.env.XDG_CACHE_HOME || join3(homedir3(), ".cache");
5177
- return join3(base, "claudinho");
5178
- }
5179
- function cachePath2() {
5180
- return join3(cacheDir2(), "state.json");
5362
+ function cachePath2(source = "espn", competition = DEFAULT_COMPETITION) {
5363
+ if (source === "espn" && competition === DEFAULT_COMPETITION) {
5364
+ return join4(cacheDir(), "state.json");
5365
+ }
5366
+ const slug = `${source}.${competition}`.replace(/[^a-zA-Z0-9._-]/g, "_");
5367
+ return join4(cacheDir(), `state.${slug}.json`);
5181
5368
  }
5182
5369
  function lockPath() {
5183
- return join3(cacheDir2(), "refresh.lock");
5370
+ return join4(cacheDir(), "refresh.lock");
5184
5371
  }
5185
- function readState() {
5372
+ function readState(source = "espn", competition = DEFAULT_COMPETITION) {
5186
5373
  try {
5187
- return JSON.parse(readFileSync3(cachePath2(), "utf8"));
5374
+ const s = JSON.parse(
5375
+ readFileSync4(cachePath2(source, competition), "utf8")
5376
+ );
5377
+ return s.version === CACHE_VERSION ? s : void 0;
5188
5378
  } catch {
5189
5379
  return void 0;
5190
5380
  }
5191
5381
  }
5192
5382
  function readCurrentState(source, competition) {
5193
- const s = readState();
5383
+ const s = readState(source, competition);
5194
5384
  return s && s.source === source && s.competition === competition ? s : void 0;
5195
5385
  }
5196
5386
  function writeState(state) {
5197
- const dir = cacheDir2();
5198
- mkdirSync3(dir, { recursive: true });
5199
- const tmp = join3(dir, `state.${process.pid}.tmp`);
5200
- writeFileSync_(tmp, JSON.stringify(state));
5201
- renameSync2(tmp, cachePath2());
5202
- }
5203
- function writeFileSync_(path, data) {
5204
- const fd = openSync(path, "w");
5205
- try {
5206
- writeSync(fd, data);
5207
- } finally {
5208
- closeSync(fd);
5209
- }
5387
+ writeFileAtomic(
5388
+ cachePath2(state.source, state.competition),
5389
+ JSON.stringify({ ...state, version: CACHE_VERSION })
5390
+ );
5391
+ }
5392
+ function backoffActive(state, now = Date.now()) {
5393
+ if (!state?.backoffUntil) return false;
5394
+ const t2 = Date.parse(state.backoffUntil);
5395
+ return Number.isFinite(t2) && now < t2;
5396
+ }
5397
+ function fixturesAttemptAgeMs(state, now = Date.now()) {
5398
+ if (!state?.fixturesAttemptedAt) return Infinity;
5399
+ const t2 = Date.parse(state.fixturesAttemptedAt);
5400
+ return Number.isFinite(t2) ? now - t2 : Infinity;
5210
5401
  }
5211
5402
  function ageMs(state, now = Date.now()) {
5212
5403
  if (!state) return Infinity;
@@ -5221,7 +5412,7 @@ function fixturesAgeMs(state, now = Date.now()) {
5221
5412
  function lockAgeMs(now = Date.now()) {
5222
5413
  const lp = lockPath();
5223
5414
  try {
5224
- const contents = readFileSync3(lp, "utf8");
5415
+ const contents = readFileSync4(lp, "utf8");
5225
5416
  const written = Number.parseInt(contents.split(/\s+/)[1] ?? "", 10);
5226
5417
  if (Number.isFinite(written)) return now - written;
5227
5418
  } catch {
@@ -5237,14 +5428,14 @@ function isLockFresh(now = Date.now()) {
5237
5428
  return lockAgeMs(now) < LOCK_STALE_MS;
5238
5429
  }
5239
5430
  function acquireLock(now = Date.now()) {
5240
- mkdirSync3(cacheDir2(), { recursive: true });
5431
+ mkdirSync2(cacheDir(), { recursive: true });
5241
5432
  const lp = lockPath();
5242
5433
  try {
5243
- const fd = openSync(lp, "wx");
5434
+ const fd = openSync2(lp, "wx");
5244
5435
  try {
5245
- writeSync(fd, `${process.pid} ${now}`);
5436
+ writeSync2(fd, `${process.pid} ${now}`);
5246
5437
  } finally {
5247
- closeSync(fd);
5438
+ closeSync2(fd);
5248
5439
  }
5249
5440
  return true;
5250
5441
  } catch {
@@ -5255,11 +5446,11 @@ function acquireLock(now = Date.now()) {
5255
5446
  return false;
5256
5447
  }
5257
5448
  try {
5258
- const fd = openSync(lp, "wx");
5449
+ const fd = openSync2(lp, "wx");
5259
5450
  try {
5260
- writeSync(fd, `${process.pid} ${now}`);
5451
+ writeSync2(fd, `${process.pid} ${now}`);
5261
5452
  } finally {
5262
- closeSync(fd);
5453
+ closeSync2(fd);
5263
5454
  }
5264
5455
  return true;
5265
5456
  } catch {
@@ -5393,9 +5584,12 @@ import { spawn } from "child_process";
5393
5584
  var MIN_REFRESH_MS = 12e3;
5394
5585
  var FIXTURES_TTL_MS = 15 * 6e4;
5395
5586
  var FIXTURES_EMPTY_TTL_MS = 6e4;
5587
+ var BACKOFF_MS = 5 * 6e4;
5588
+ var BACKOFF_JITTER_MS = 6e4;
5396
5589
  function fixturesStale(state, now) {
5397
5590
  const ttl = (state?.fixtures?.length ?? 0) > 0 ? FIXTURES_TTL_MS : FIXTURES_EMPTY_TTL_MS;
5398
- return fixturesAgeMs(state, now) > ttl;
5591
+ if (fixturesAgeMs(state, now) <= ttl) return false;
5592
+ return fixturesAttemptAgeMs(state, now) > FIXTURES_EMPTY_TTL_MS;
5399
5593
  }
5400
5594
  function nextStaticUpcoming(nowMs) {
5401
5595
  return [...allFixtures()].sort(byKickoff).find((m) => Date.parse(m.kickoff) >= nowMs);
@@ -5409,24 +5603,57 @@ function liveWindowActive(nowMs) {
5409
5603
  if (resolveCompetition() !== DEFAULT_COMPETITION) return true;
5410
5604
  return inLiveWindow(nowMs);
5411
5605
  }
5412
- function liveAdapter() {
5606
+ function liveAdapter(source) {
5413
5607
  const competition = resolveCompetition();
5414
- if (competition === DEFAULT_COMPETITION) {
5608
+ if (source === "espn" && competition === DEFAULT_COMPETITION) {
5415
5609
  return new EspnAdapter({ enrichGroups: false });
5416
5610
  }
5417
- return makeAdapter("espn");
5611
+ return makeAdapter(source);
5418
5612
  }
5419
5613
  async function runRefresh(opts = {}) {
5420
5614
  const now = opts.now ?? /* @__PURE__ */ new Date();
5421
5615
  const nowMs = now.getTime();
5422
5616
  const source = opts.source ?? "espn";
5423
5617
  const competition = resolveCompetition();
5424
- const cached = readState();
5618
+ if (!KNOWN_SOURCES.includes(source)) {
5619
+ if (!readState(source, competition) && acquireLock()) {
5620
+ try {
5621
+ writeState({
5622
+ updatedAt: now.toISOString(),
5623
+ live: [],
5624
+ degraded: true,
5625
+ // no live provider served this scope — never claim otherwise
5626
+ source,
5627
+ competition
5628
+ });
5629
+ } finally {
5630
+ releaseLock();
5631
+ }
5632
+ }
5633
+ return;
5634
+ }
5635
+ const cached = readState(source, competition);
5425
5636
  const sameScope = !!cached && cached.source === source && cached.competition === competition;
5426
5637
  const base = sameScope ? cached : void 0;
5427
- const needLive = liveWindowActive(nowMs) && (!base || ageMs(base, nowMs) >= MIN_REFRESH_MS);
5428
- const needFixtures = inKnockoutPhase(nowMs) && fixturesStale(base, nowMs);
5429
- if (!needLive && !needFixtures) return;
5638
+ const inBackoff = backoffActive(base, nowMs);
5639
+ const needLive = !inBackoff && liveWindowActive(nowMs) && (!base || ageMs(base, nowMs) >= MIN_REFRESH_MS);
5640
+ const needFixtures = !inBackoff && inKnockoutPhase(nowMs) && fixturesStale(base, nowMs);
5641
+ if (!needLive && !needFixtures) {
5642
+ if (!base && acquireLock()) {
5643
+ try {
5644
+ writeState({
5645
+ updatedAt: now.toISOString(),
5646
+ live: [],
5647
+ degraded: false,
5648
+ source,
5649
+ competition
5650
+ });
5651
+ } finally {
5652
+ releaseLock();
5653
+ }
5654
+ }
5655
+ return;
5656
+ }
5430
5657
  if (!acquireLock()) return;
5431
5658
  try {
5432
5659
  let live = base?.live ?? [];
@@ -5434,9 +5661,12 @@ async function runRefresh(opts = {}) {
5434
5661
  let updatedAt = base?.updatedAt ?? now.toISOString();
5435
5662
  let fixtures = base?.fixtures;
5436
5663
  let fixturesUpdatedAt = base?.fixturesUpdatedAt;
5664
+ let fixturesAttemptedAt = base?.fixturesAttemptedAt;
5665
+ let backoffUntil = base?.backoffUntil;
5666
+ const adapter = liveAdapter(source);
5437
5667
  if (needLive) {
5438
5668
  try {
5439
- const r = await getLiveMatches(liveAdapter(), now);
5669
+ const r = await getLiveMatches(adapter, now);
5440
5670
  live = r.matches;
5441
5671
  degraded = r.degraded;
5442
5672
  } catch {
@@ -5445,8 +5675,9 @@ async function runRefresh(opts = {}) {
5445
5675
  updatedAt = now.toISOString();
5446
5676
  }
5447
5677
  if (needFixtures) {
5678
+ fixturesAttemptedAt = now.toISOString();
5448
5679
  try {
5449
- const r = await getKnockoutFixtures(liveAdapter(), now);
5680
+ const r = await getKnockoutFixtures(adapter, now);
5450
5681
  if (!r.degraded) {
5451
5682
  fixtures = r.fixtures;
5452
5683
  fixturesUpdatedAt = now.toISOString();
@@ -5454,6 +5685,13 @@ async function runRefresh(opts = {}) {
5454
5685
  } catch {
5455
5686
  }
5456
5687
  }
5688
+ if (adapter.lastError?.throttled) {
5689
+ backoffUntil = new Date(
5690
+ nowMs + BACKOFF_MS + Math.floor(Math.random() * BACKOFF_JITTER_MS)
5691
+ ).toISOString();
5692
+ } else if (backoffUntil && Date.parse(backoffUntil) <= nowMs) {
5693
+ backoffUntil = void 0;
5694
+ }
5457
5695
  writeState({
5458
5696
  updatedAt,
5459
5697
  live,
@@ -5461,19 +5699,23 @@ async function runRefresh(opts = {}) {
5461
5699
  source,
5462
5700
  competition,
5463
5701
  ...fixtures ? { fixtures } : {},
5464
- ...fixturesUpdatedAt ? { fixturesUpdatedAt } : {}
5702
+ ...fixturesUpdatedAt ? { fixturesUpdatedAt } : {},
5703
+ ...fixturesAttemptedAt ? { fixturesAttemptedAt } : {},
5704
+ ...backoffUntil ? { backoffUntil } : {}
5465
5705
  });
5466
5706
  } finally {
5467
5707
  releaseLock();
5468
5708
  }
5469
5709
  }
5470
- function shouldRefresh(now = Date.now()) {
5710
+ function shouldRefresh(now = Date.now(), state = readState("espn", resolveCompetition())) {
5471
5711
  if (!liveWindowActive(now)) return false;
5712
+ if (backoffActive(state, now)) return false;
5472
5713
  if (isLockFresh(now)) return false;
5473
- return ageMs(readState(), now) > LIVE_TTL_MS;
5714
+ return ageMs(state, now) > LIVE_TTL_MS;
5474
5715
  }
5475
- function shouldRefreshFixtures(now = Date.now(), state = readState()) {
5716
+ function shouldRefreshFixtures(now = Date.now(), state = readState("espn", resolveCompetition())) {
5476
5717
  if (!inKnockoutPhase(now)) return false;
5718
+ if (backoffActive(state, now)) return false;
5477
5719
  if (isLockFresh(now)) return false;
5478
5720
  return fixturesStale(state, now);
5479
5721
  }
@@ -5493,74 +5735,15 @@ function spawnRefresh(source) {
5493
5735
  }
5494
5736
  }
5495
5737
 
5496
- // src/cursorPayload.ts
5497
- import { readFileSync as readFileSync4 } from "fs";
5498
- function parseCursorPayload(raw) {
5499
- try {
5500
- const trimmed = raw.trim();
5501
- if (!trimmed) return void 0;
5502
- return JSON.parse(trimmed);
5503
- } catch {
5504
- return void 0;
5505
- }
5506
- }
5507
- function readCursorPayload() {
5508
- try {
5509
- if (process.stdin.isTTY) return void 0;
5510
- return parseCursorPayload(readFileSync4(0, "utf8"));
5511
- } catch {
5512
- return void 0;
5513
- }
5514
- }
5515
- function looksLikeCursorPayload(payload) {
5516
- return !!(payload.model?.display_name || payload.context_window != null || payload.render_width_chars != null || payload.worktree?.name || payload.vim?.mode);
5517
- }
5518
- function cursorMetaEnabled(payload) {
5519
- const v = (process.env.CLAUDINHO_CURSOR_META ?? "").toLowerCase();
5520
- if (v === "0" || v === "false" || v === "no") return false;
5521
- if (v === "1" || v === "true" || v === "yes") return !!payload;
5522
- if (v === "auto") return !!payload && looksLikeCursorPayload(payload);
5523
- return false;
5524
- }
5525
- function renderCursorMetaLine(payload) {
5526
- const parts = [];
5527
- const model = payload.model?.display_name;
5528
- if (model) {
5529
- let label = model;
5530
- if (payload.model?.param_summary) label += ` ${payload.model.param_summary}`;
5531
- parts.push(label);
5532
- }
5533
- const pct2 = payload.context_window?.used_percentage;
5534
- if (typeof pct2 === "number" && Number.isFinite(pct2)) {
5535
- parts.push(`ctx ${Math.floor(pct2)}%`);
5536
- }
5537
- if (payload.worktree?.name) parts.push(`wt ${payload.worktree.name}`);
5538
- if (payload.vim?.mode) parts.push(payload.vim.mode);
5539
- if (parts.length === 0) return void 0;
5540
- return `\x1B[90m${parts.join(" ")}\x1B[0m`;
5541
- }
5542
- function renderPromptOutput(scoreLine, payload) {
5543
- const meta = payload && cursorMetaEnabled(payload) ? renderCursorMetaLine(payload) : void 0;
5544
- if (meta) return `${scoreLine}
5545
- ${meta}`;
5546
- return scoreLine;
5547
- }
5548
-
5549
5738
  // src/install.ts
5550
- import {
5551
- copyFileSync,
5552
- existsSync,
5553
- mkdirSync as mkdirSync4,
5554
- readFileSync as readFileSync5,
5555
- writeFileSync as writeFileSync3
5556
- } from "fs";
5557
- import { homedir as homedir4 } from "os";
5558
- import { dirname as dirname2, join as join4 } from "path";
5739
+ import { copyFileSync, existsSync, readFileSync as readFileSync5 } from "fs";
5740
+ import { homedir as homedir2 } from "os";
5741
+ import { join as join5 } from "path";
5559
5742
  function claudeSettingsPath() {
5560
- return join4(homedir4(), ".claude", "settings.json");
5743
+ return join5(homedir2(), ".claude", "settings.json");
5561
5744
  }
5562
5745
  function cursorCliConfigPath() {
5563
- return join4(homedir4(), ".cursor", "cli-config.json");
5746
+ return join5(homedir2(), ".cursor", "cli-config.json");
5564
5747
  }
5565
5748
  function isSameCommand(configured, requested) {
5566
5749
  return configured === requested;
@@ -5622,8 +5805,7 @@ function initStatuslineFor(target, opts = {}) {
5622
5805
  }
5623
5806
  backupOnce(path);
5624
5807
  settings.statusLine = sl;
5625
- mkdirSync4(dirname2(path), { recursive: true });
5626
- writeFileSync3(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
5808
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n");
5627
5809
  const surface = target === "cursor" ? "Cursor CLI statusline" : "Statusline";
5628
5810
  return {
5629
5811
  action: "written",
@@ -5669,8 +5851,7 @@ function initHook(opts = {}) {
5669
5851
  const hooks = settings.hooks;
5670
5852
  hooks[CLAUDE_HOOK_EVENT] ??= [];
5671
5853
  hooks[CLAUDE_HOOK_EVENT].push({ hooks: [{ type: "command", command }] });
5672
- mkdirSync4(dirname2(path), { recursive: true });
5673
- writeFileSync3(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
5854
+ writeFileAtomic(path, JSON.stringify(settings, null, 2) + "\n");
5674
5855
  return {
5675
5856
  action: "written",
5676
5857
  path,
@@ -5741,6 +5922,11 @@ function precheck(cfg, t2, date) {
5741
5922
  if (cfg.tz && !isValidTimeZone(cfg.tz)) {
5742
5923
  process.stderr.write(t2("warn.tz", { tz: cfg.tz }) + "\n");
5743
5924
  }
5925
+ if (!KNOWN_SOURCES.includes(cfg.source)) {
5926
+ throw new InputError(
5927
+ t2("err.source", { source: cfg.source, sources: KNOWN_SOURCES.join(", ") })
5928
+ );
5929
+ }
5744
5930
  const competition = resolveCompetition();
5745
5931
  if (competition !== DEFAULT_COMPETITION) {
5746
5932
  process.stderr.write(
@@ -6021,9 +6207,9 @@ async function cmdBracket(stage, opts, ctx) {
6021
6207
  out(disclaimer(t2, c));
6022
6208
  maybeStarNudge(ctx);
6023
6209
  }
6024
- function cmdPrompt({ cfg }) {
6210
+ function cmdPrompt({ cfg }, io = {}) {
6025
6211
  try {
6026
- const payload = readCursorPayload();
6212
+ const payload = "cursor" in io ? io.cursor : readCursorPayload();
6027
6213
  const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
6028
6214
  const compact = !["0", "false", "no"].includes(
6029
6215
  (process.env.CLAUDINHO_COMPACT ?? "").toLowerCase()
@@ -6033,7 +6219,7 @@ function cmdPrompt({ cfg }) {
6033
6219
  const state = readCurrentState(cfg.source, resolveCompetition());
6034
6220
  const scoreLine = renderPrompt(state, { team, compact, max, flags: flagsEnabled() });
6035
6221
  out(renderPromptOutput(scoreLine, payload));
6036
- if (!state || shouldRefresh() || shouldRefreshFixtures(Date.now(), state)) {
6222
+ if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
6037
6223
  spawnRefresh(cfg.source);
6038
6224
  }
6039
6225
  } catch {
@@ -6046,7 +6232,7 @@ function cmdHook({ cfg }) {
6046
6232
  const state = readCurrentState(cfg.source, resolveCompetition());
6047
6233
  const ctx = renderHook(state, { team, flags: flagsEnabled() });
6048
6234
  if (ctx) out(ctx);
6049
- if (!state || shouldRefresh() || shouldRefreshFixtures(Date.now(), state)) {
6235
+ if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
6050
6236
  spawnRefresh(cfg.source);
6051
6237
  }
6052
6238
  } catch {
@@ -6663,7 +6849,7 @@ function handlePipeError(stream) {
6663
6849
  }
6664
6850
  handlePipeError(process.stdout);
6665
6851
  handlePipeError(process.stderr);
6666
- var VERSION = "0.8.19";
6852
+ var VERSION = "0.9.0";
6667
6853
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
6668
6854
  function ctxFrom(cmd) {
6669
6855
  let root = cmd;
@@ -6749,8 +6935,9 @@ program.command("share").description("print a shareable, copy-pasteable match sn
6749
6935
  fail(e);
6750
6936
  }
6751
6937
  });
6752
- program.command("prompt").description("print a status line (Claude Code / Cursor CLI statusline, tmux, Starship, \u2026)").action((_opts, cmd) => {
6753
- cmdPrompt(ctxFrom(cmd));
6938
+ program.command("prompt").description("print a status line (Claude Code / Cursor CLI statusline, tmux, Starship, \u2026)").action(async (_opts, cmd) => {
6939
+ const cursor = await readCursorPayloadBounded();
6940
+ cmdPrompt(ctxFrom(cmd), { cursor });
6754
6941
  });
6755
6942
  var init = program.command("init").description("one-step setup for your agent: `init cursor` or `init claude`");
6756
6943
  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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.8.19",
3
+ "version": "0.9.0",
4
4
  "description": "Live scores, fixtures, group tables, and read-only prediction-market signals for the 2026 men's football tournament — in your terminal, Claude Code, and Cursor CLI statusline. No API keys. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,10 +58,11 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.20.0",
61
+ "@vitest/coverage-v8": "^4.1.9",
61
62
  "tsup": "^8.0.0",
62
63
  "typescript": "^5.7.0",
63
64
  "vitest": "^4.1.9",
64
- "@claudinho/core": "0.8.19"
65
+ "@claudinho/core": "0.9.0"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "tsup",