@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.
- package/dist/index.js +368 -181
- 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
|
-
|
|
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 = "
|
|
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
|
|
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
|
|
3124
|
-
*
|
|
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 =
|
|
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 =
|
|
3143
|
-
|
|
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 ??
|
|
3203
|
+
this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
3152
3204
|
);
|
|
3205
|
+
this.lastError = void 0;
|
|
3153
3206
|
try {
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3937
|
-
|
|
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
|
|
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
|
-
|
|
3979
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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] ??
|
|
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 {
|
|
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
|
|
5255
|
+
return join2(cacheDir(), "market-signals.json");
|
|
5062
5256
|
}
|
|
5063
5257
|
function readFile() {
|
|
5064
5258
|
try {
|
|
5065
|
-
return JSON.parse(
|
|
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
|
-
|
|
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 {
|
|
5106
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
5165
|
-
openSync,
|
|
5166
|
-
readFileSync as
|
|
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 {
|
|
5173
|
-
|
|
5359
|
+
import { join as join4 } from "path";
|
|
5360
|
+
var CACHE_VERSION = 2;
|
|
5174
5361
|
var LOCK_STALE_MS = 6e4;
|
|
5175
|
-
function
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
}
|
|
5179
|
-
|
|
5180
|
-
return
|
|
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
|
|
5370
|
+
return join4(cacheDir(), "refresh.lock");
|
|
5184
5371
|
}
|
|
5185
|
-
function readState() {
|
|
5372
|
+
function readState(source = "espn", competition = DEFAULT_COMPETITION) {
|
|
5186
5373
|
try {
|
|
5187
|
-
|
|
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
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
const
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
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 =
|
|
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
|
-
|
|
5431
|
+
mkdirSync2(cacheDir(), { recursive: true });
|
|
5241
5432
|
const lp = lockPath();
|
|
5242
5433
|
try {
|
|
5243
|
-
const fd =
|
|
5434
|
+
const fd = openSync2(lp, "wx");
|
|
5244
5435
|
try {
|
|
5245
|
-
|
|
5436
|
+
writeSync2(fd, `${process.pid} ${now}`);
|
|
5246
5437
|
} finally {
|
|
5247
|
-
|
|
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 =
|
|
5449
|
+
const fd = openSync2(lp, "wx");
|
|
5259
5450
|
try {
|
|
5260
|
-
|
|
5451
|
+
writeSync2(fd, `${process.pid} ${now}`);
|
|
5261
5452
|
} finally {
|
|
5262
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
5428
|
-
const
|
|
5429
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
5552
|
-
|
|
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
|
|
5743
|
+
return join5(homedir2(), ".claude", "settings.json");
|
|
5561
5744
|
}
|
|
5562
5745
|
function cursorCliConfigPath() {
|
|
5563
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
65
|
+
"@claudinho/core": "0.9.0"
|
|
65
66
|
},
|
|
66
67
|
"scripts": {
|
|
67
68
|
"build": "tsup",
|