@claudinho/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +35 -3
  2. package/dist/index.js +697 -42
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @claudinho/cli ⚽
2
2
 
3
- **The 2026 men's football tournament, right in your terminal.** Live scores, fixtures, and group tables — TZ-aware, localized, scriptable.
3
+ **The 2026 men's football tournament, right in your terminal.** Live scores, fixtures, group tables, and market odds — TZ-aware, localized, scriptable.
4
4
 
5
5
  > ⚠️ **Not affiliated with, endorsed by, or connected to FIFA or Anthropic.**
6
6
  > Claudinho is an independent, open-source fan project. It shows factual match
@@ -23,6 +23,7 @@ claudinho live # matches in play right now
23
23
  claudinho next <TEAM> # a team's next fixture + countdown (e.g. next MEX)
24
24
  claudinho table [GROUP] # group standings (default: all groups)
25
25
  claudinho match <id> # a single match's detail
26
+ claudinho markets [target] # prediction-market odds: today | <date> | <id> | next <TEAM>
26
27
  claudinho prompt # one compact status line (for statusline/tmux/Starship)
27
28
  claudinho init-statusline # wire it into the Claude Code statusline
28
29
  claudinho hook # live-score context for a Claude Code hook (silent off-match)
@@ -50,6 +51,7 @@ claudinho today --flavor off # just the facts, no commentary
50
51
  | `--no-color` | disable ANSI color (also honors `NO_COLOR`; auto-off when piped) |
51
52
  | `--source <name>` | live data provider (advanced; sensible default) |
52
53
  | `--flavor <level>` | commentary flair: `off`, `subtle`, `full` (default: `full`; also `CLAUDINHO_FLAVOR`) |
54
+ | `--no-markets` | hide prediction-market signals in `today`/`match` (also `CLAUDINHO_MARKETS=off`) |
53
55
 
54
56
  Team codes are 3-letter (FIFA/IOC-style): `MEX`, `BRA`, `USA`, `ENG`, …
55
57
 
@@ -64,6 +66,35 @@ localized per `--lang`, and they never affect `--json` output.
64
66
  - `--flavor subtle` — only goals and full-time
65
67
  - `--flavor off` — just the facts
66
68
 
69
+ ## Prediction-market signals
70
+
71
+ `claudinho markets` shows **read-only** prediction-market odds — "who's favored" as
72
+ market-implied percentages — for a date, a match, or a team's next fixture:
73
+
74
+ ```bash
75
+ claudinho markets # today's signals
76
+ claudinho markets 2026-06-11 # a specific date
77
+ claudinho markets 760415 # one match by id
78
+ claudinho markets next MEX # a team's next fixture
79
+ claudinho markets today --json # structured sidecar output
80
+ ```
81
+
82
+ A short market line is also added under `claudinho today` and `claudinho match`
83
+ when a reliable market is available. It's **informational only — not betting
84
+ advice:** market-implied percentages with attribution, no trading, no links. Data
85
+ comes from Polymarket public market data and is shown
86
+ only when the market maps cleanly to the result and is fresh.
87
+
88
+ Opt out with `--no-markets` (per command) or `CLAUDINHO_MARKETS=off` (global). The
89
+ statusline and hook **never** show market data — it stays off the hot path.
90
+
91
+ > **How matches are matched:** event slugs are derived automatically from each
92
+ > fixture (`fifwc-{home}-{away}-{date}`), so real odds appear for any match with a
93
+ > live Polymarket market — no mapping needed (`mapping.2026.json` is for slug
94
+ > *overrides* only). Matching fails closed, so an unmatched fixture simply shows
95
+ > nothing. For an offline preview, set `CLAUDINHO_MARKETS_SOURCE=fake` to render
96
+ > clearly-labeled synthetic **"demo data"** odds.
97
+
67
98
  ## Statusline (Claude Code)
68
99
 
69
100
  ```bash
@@ -111,8 +142,9 @@ statusline/hook cache is keyed to the active competition, so switching with
111
142
 
112
143
  The full fixture list (104 matches, groups, venues, host cities, kickoffs) ships **bundled**
113
144
  in the package, so the common path is offline and instant. Only live match
114
- state hits the network. Scores come from a swappable data provider; provider
115
- attribution and rate limits are respected.
145
+ state hits the network. Live scores come from **ESPN's** public scoreboard (a
146
+ swappable provider, attributed in output as `Live data: ESPN`) and market odds
147
+ from Polymarket; provider attribution and rate limits are respected.
116
148
 
117
149
  ## License
118
150
 
package/dist/index.js CHANGED
@@ -2830,12 +2830,16 @@ function mergeLive(base, live) {
2830
2830
  for (const m of live) byId.set(m.id, m);
2831
2831
  return [...byId.values()];
2832
2832
  }
2833
+ function liveSourceLabel(source) {
2834
+ const known = { espn: "ESPN" };
2835
+ return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
2836
+ }
2833
2837
  async function getMatchesForDate(adapter, dateISO) {
2834
2838
  const base = allFixtures();
2835
2839
  const day = dateISO.slice(0, 10);
2836
2840
  try {
2837
2841
  const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
2838
- return { matches: mergeLive(base, live), degraded: false };
2842
+ return { matches: mergeLive(base, live), degraded: false, source: adapter.name };
2839
2843
  } catch {
2840
2844
  return { matches: base, degraded: true };
2841
2845
  }
@@ -2846,11 +2850,425 @@ function shiftUtcDate(dateISO, days) {
2846
2850
  }
2847
2851
  async function getLiveMatches(adapter) {
2848
2852
  try {
2849
- return { matches: await adapter.fetchLive(), degraded: false };
2853
+ return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
2850
2854
  } catch {
2851
2855
  return { matches: [], degraded: true };
2852
2856
  }
2853
2857
  }
2858
+ var DEFAULT_MAX_AGE_MS = 15 * 6e4;
2859
+ function normalizeOutcomes(outcomes) {
2860
+ const sum = outcomes.reduce(
2861
+ (s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
2862
+ 0
2863
+ );
2864
+ if (sum <= 0) return outcomes.map((o) => ({ ...o, probability: 0 }));
2865
+ return outcomes.map((o) => ({
2866
+ ...o,
2867
+ probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
2868
+ }));
2869
+ }
2870
+ function favoriteStrength(probability) {
2871
+ if (probability >= 0.65) return "clear";
2872
+ if (probability >= 0.52) return "slight";
2873
+ return "close";
2874
+ }
2875
+ function deriveFavorite(outcomes) {
2876
+ let top;
2877
+ for (const o of outcomes) {
2878
+ if (o.kind === "other") continue;
2879
+ if (!top || o.probability > top.probability) top = o;
2880
+ }
2881
+ if (!top || top.probability <= 0 || top.kind === "other") return void 0;
2882
+ return {
2883
+ kind: top.kind,
2884
+ teamCode: top.teamCode,
2885
+ probability: top.probability,
2886
+ strength: favoriteStrength(top.probability)
2887
+ };
2888
+ }
2889
+ function mapsCleanly(match, outcomes) {
2890
+ if (outcomes.some((o) => o.kind === "other")) return false;
2891
+ const home = outcomes.find((o) => o.kind === "home");
2892
+ const away = outcomes.find((o) => o.kind === "away");
2893
+ const draw = outcomes.find((o) => o.kind === "draw");
2894
+ if (!home || !away) return false;
2895
+ if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
2896
+ return false;
2897
+ }
2898
+ if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
2899
+ return false;
2900
+ }
2901
+ if (match.stage === "GROUP" && !draw) return false;
2902
+ return true;
2903
+ }
2904
+ function hasSaneDistribution(outcomes) {
2905
+ const priced = outcomes.filter((o) => Number.isFinite(o.probability) && o.probability > 0);
2906
+ if (priced.length < 2) return false;
2907
+ const sum = priced.reduce((s, o) => s + o.probability, 0);
2908
+ return sum > 0.97 && sum < 1.03;
2909
+ }
2910
+ function isStaleSignal(signal, options = {}) {
2911
+ const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
2912
+ const asOf = Date.parse(signal.asOf);
2913
+ if (!Number.isFinite(asOf)) return true;
2914
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
2915
+ return now - asOf > maxAge;
2916
+ }
2917
+ function isReliableMarketSignal(signal, options = {}) {
2918
+ if (options.includeUnreliable) return true;
2919
+ if (signal.ambiguous) return false;
2920
+ if (!signal.favorite) return false;
2921
+ if (!hasSaneDistribution(signal.outcomes)) return false;
2922
+ if (signal.stale || isStaleSignal(signal, options)) return false;
2923
+ if (options.minLiquidity != null) {
2924
+ if (signal.liquidity == null || signal.liquidity < options.minLiquidity) return false;
2925
+ }
2926
+ return true;
2927
+ }
2928
+ function buildMarketSignal(input) {
2929
+ const outcomes = normalizeOutcomes(input.outcomes);
2930
+ const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
2931
+ const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
2932
+ const signal = {
2933
+ matchId: input.match.id,
2934
+ source: input.source,
2935
+ sourceMarketId: input.sourceMarketId,
2936
+ asOf: input.asOf,
2937
+ fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2938
+ outcomes,
2939
+ favorite,
2940
+ liquidity: input.liquidity,
2941
+ volume24h: input.volume24h,
2942
+ stale: false,
2943
+ ambiguous
2944
+ };
2945
+ signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
2946
+ return signal;
2947
+ }
2948
+ function pct(p) {
2949
+ return Math.round(p * 100);
2950
+ }
2951
+ function marketSourceLabel(source) {
2952
+ if (source === "polymarket") return "Polymarket";
2953
+ if (source === "fake") return "demo data";
2954
+ return source.charAt(0).toUpperCase() + source.slice(1);
2955
+ }
2956
+ function outcomeLabel(o, match) {
2957
+ if (o.kind === "home") return match.home.name;
2958
+ if (o.kind === "away") return match.away.name;
2959
+ if (o.kind === "draw") return "Draw";
2960
+ return o.label;
2961
+ }
2962
+ function utcHhmm(iso) {
2963
+ const t = Date.parse(iso);
2964
+ if (!Number.isFinite(t)) return "";
2965
+ return `${new Date(t).toISOString().slice(11, 16)} UTC`;
2966
+ }
2967
+ function marketFavoriteText(signal, match) {
2968
+ const fav = signal.favorite;
2969
+ if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
2970
+ if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
2971
+ const name = fav.kind === "home" ? match.home.name : match.away.name;
2972
+ return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
2973
+ }
2974
+ function marketProbabilityText(signal, match) {
2975
+ const order = ["home", "draw", "away"];
2976
+ const parts = [];
2977
+ for (const kind of order) {
2978
+ const o = signal.outcomes.find((x) => x.kind === kind);
2979
+ if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
2980
+ }
2981
+ for (const o of signal.outcomes) {
2982
+ if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
2983
+ }
2984
+ return parts.join(" \xB7 ");
2985
+ }
2986
+ function marketAttributionText(signal) {
2987
+ const time = utcHhmm(signal.asOf);
2988
+ const src = `Source: ${marketSourceLabel(signal.source)}`;
2989
+ return time ? `${src} \xB7 updated ${time}` : src;
2990
+ }
2991
+ function marketLine(signal, match) {
2992
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
2993
+ signal.source
2994
+ )} \xB7 informational only`;
2995
+ }
2996
+ function marketBlock(signal, match) {
2997
+ const lines = [];
2998
+ if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
2999
+ lines.push(marketFavoriteText(signal, match));
3000
+ lines.push(marketProbabilityText(signal, match));
3001
+ lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
3002
+ return lines;
3003
+ }
3004
+ var FakeMarketProvider = class {
3005
+ constructor(opts = {}) {
3006
+ this.opts = opts;
3007
+ }
3008
+ opts;
3009
+ name = "fake";
3010
+ async findSignal(match, options) {
3011
+ const preset = this.opts.signals?.[match.id];
3012
+ if (preset) return preset;
3013
+ if (this.opts.synthesize) return this.synthesize(match, options);
3014
+ return void 0;
3015
+ }
3016
+ async findSignals(matches, options) {
3017
+ const signals = /* @__PURE__ */ new Map();
3018
+ const checked = /* @__PURE__ */ new Set();
3019
+ for (const m of matches) {
3020
+ checked.add(m.id);
3021
+ const s = await this.findSignal(m, options);
3022
+ if (s) signals.set(m.id, s);
3023
+ }
3024
+ return { signals, checked };
3025
+ }
3026
+ synthesize(match, options) {
3027
+ const seed = hash(`${match.home.code}-${match.away.code}`);
3028
+ const home = 0.3 + seed % 33 / 100;
3029
+ const away = 0.18 + (seed >> 3) % 23 / 100;
3030
+ const draw = Math.max(0.05, 1 - home - away);
3031
+ const outcomes = [
3032
+ { kind: "home", teamCode: match.home.code, label: match.home.name, probability: home },
3033
+ { kind: "draw", label: "Draw", probability: draw },
3034
+ { kind: "away", teamCode: match.away.code, label: match.away.name, probability: away }
3035
+ ];
3036
+ const now = this.opts.now ?? options?.now ?? /* @__PURE__ */ new Date();
3037
+ const asOf = new Date(now.getTime() - 6e4).toISOString();
3038
+ return buildMarketSignal({
3039
+ match,
3040
+ source: "fake",
3041
+ sourceMarketId: `fake-${match.id}`,
3042
+ asOf,
3043
+ fetchedAt: now.toISOString(),
3044
+ outcomes,
3045
+ liquidity: 5e4,
3046
+ now,
3047
+ maxAgeMs: options?.maxAgeMs
3048
+ });
3049
+ }
3050
+ };
3051
+ function hash(s) {
3052
+ let h = 0;
3053
+ for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) >>> 0;
3054
+ return h % 1e5;
3055
+ }
3056
+ var mapping_2026_default = {
3057
+ version: 1,
3058
+ note: "Optional matchId -> Polymarket EVENT-slug OVERRIDES. By default the slug is DERIVED from each fixture (fifwc-{home}-{away}-{UTC-date}, e.g. fifwc-mex-rsa-2026-06-11), so most matches need NO entry here. Add an entry only for a fixture whose real Polymarket slug differs (e.g. a team abbreviation that isn't the FIFA code, or a tz-shifted date). The event payload carries the three moneyline binary markets; each outcome's probability is its 'Yes' price; validation fails closed. Entry: { eventSlug, eventId? }.",
3059
+ markets: {}
3060
+ };
3061
+ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
3062
+ var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
3063
+ var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
3064
+ var DEFAULT_TIMEOUT_MS = 8e3;
3065
+ var WC_SERIES_SLUG = "soccer-fifwc";
3066
+ var WC_SPORT = "fifwc";
3067
+ var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
3068
+ var NON_REGULAR_TIME = /extra time|penalt|to advance|to qualif|win the (group|tournament|cup|title)/i;
3069
+ var BUNDLED_MAPPING = mapping_2026_default.markets;
3070
+ var PolymarketProvider = class {
3071
+ constructor(opts = {}) {
3072
+ this.opts = opts;
3073
+ }
3074
+ opts;
3075
+ name = "polymarket";
3076
+ async findSignal(match, options) {
3077
+ return (await this.resolveOne(match, options)).signal;
3078
+ }
3079
+ async findSignals(matches, options) {
3080
+ const signals = /* @__PURE__ */ new Map();
3081
+ const checked = /* @__PURE__ */ new Set();
3082
+ const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
3083
+ for (const m of matches) {
3084
+ if (Date.now() >= deadline) break;
3085
+ const r = await this.resolveOne(m, options);
3086
+ if (r.checked) checked.add(m.id);
3087
+ if (r.signal) signals.set(m.id, r.signal);
3088
+ }
3089
+ return { signals, checked };
3090
+ }
3091
+ /**
3092
+ * Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
3093
+ * source and found no usable market, or the fixture is unmappable) from a
3094
+ * provider/network error — so transient failures are retried, not
3095
+ * negative-cached.
3096
+ */
3097
+ async resolveOne(match, options) {
3098
+ const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
3099
+ const eventSlug = entry?.eventSlug ?? deriveEventSlug(match);
3100
+ if (!eventSlug) return { checked: true };
3101
+ try {
3102
+ const event = await this.fetchEvent(eventSlug, options?.timeoutMs);
3103
+ const signal = event ? this.toSignal(match, eventSlug, event, options) : void 0;
3104
+ return { signal, checked: true };
3105
+ } catch {
3106
+ return { checked: false };
3107
+ }
3108
+ }
3109
+ async fetchEvent(slug, timeoutMs) {
3110
+ const base = this.opts.baseUrl ?? DEFAULT_BASE2;
3111
+ assertAllowedHost(base);
3112
+ const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
3113
+ const doFetch = this.opts.fetchImpl ?? fetch;
3114
+ const res = await doFetch(url, {
3115
+ signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
3116
+ headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
3117
+ });
3118
+ if (res.status === 404) return void 0;
3119
+ if (!res.ok) {
3120
+ throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
3121
+ }
3122
+ const data = await res.json();
3123
+ const event = Array.isArray(data) ? data[0] : data;
3124
+ return event && typeof event === "object" ? event : void 0;
3125
+ }
3126
+ toSignal(match, eventSlug, event, options) {
3127
+ if (event.active === false || event.closed === true) return void 0;
3128
+ if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
3129
+ return void 0;
3130
+ }
3131
+ if (event.slug != null && event.slug !== eventSlug) return void 0;
3132
+ const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
3133
+ const kick = Date.parse(match.kickoff);
3134
+ if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
3135
+ return void 0;
3136
+ }
3137
+ const moneyline = (event.markets ?? []).filter(
3138
+ (m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
3139
+ );
3140
+ const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
3141
+ const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
3142
+ const drawMarket = pickDraw(moneyline);
3143
+ if (!homeMarket || !awayMarket) return void 0;
3144
+ const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
3145
+ if (new Set(legIds).size !== legIds.length) return void 0;
3146
+ const legs = [
3147
+ ["home", homeMarket, match.home.code, match.home.name],
3148
+ ["draw", drawMarket, void 0, "Draw"],
3149
+ ["away", awayMarket, match.away.code, match.away.name]
3150
+ ];
3151
+ const outcomes = [];
3152
+ let asOf = event.updatedAt;
3153
+ let liquidity;
3154
+ for (const [kind, market, teamCode, label] of legs) {
3155
+ if (!market) continue;
3156
+ if (market.closed === true || market.active === false) return void 0;
3157
+ if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
3158
+ const yes = yesPrice(market);
3159
+ if (yes == null) return void 0;
3160
+ outcomes.push({ kind, teamCode, label, probability: yes });
3161
+ if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
3162
+ const liq = numberish(market.liquidityNum ?? market.liquidity);
3163
+ if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
3164
+ }
3165
+ const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
3166
+ if (rawSum < 0.9 || rawSum > 1.15) return void 0;
3167
+ const signal = buildMarketSignal({
3168
+ match,
3169
+ source: "polymarket",
3170
+ sourceMarketId: event.id ?? eventSlug,
3171
+ asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
3172
+ outcomes,
3173
+ liquidity,
3174
+ now: options?.now ?? this.opts.now,
3175
+ maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
3176
+ });
3177
+ return signal.ambiguous ? void 0 : signal;
3178
+ }
3179
+ };
3180
+ function deriveEventSlug(match) {
3181
+ const home = match.home.code.toLowerCase();
3182
+ const away = match.away.code.toLowerCase();
3183
+ if (home === away || home === "tbd" || away === "tbd") return void 0;
3184
+ if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return void 0;
3185
+ const date = match.kickoff.slice(0, 10);
3186
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return void 0;
3187
+ return `fifwc-${home}-${away}-${date}`;
3188
+ }
3189
+ function slugToken(m) {
3190
+ return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
3191
+ }
3192
+ function isDrawMarket(m) {
3193
+ return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
3194
+ }
3195
+ function pickMarket(markets, teamCode, teamName) {
3196
+ const code = teamCode.toLowerCase();
3197
+ const name = teamName.trim().toLowerCase();
3198
+ const teamMarkets = markets.filter((m) => !isDrawMarket(m));
3199
+ const bySlug = teamMarkets.find((m) => slugToken(m) === code);
3200
+ if (bySlug) return bySlug;
3201
+ return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
3202
+ }
3203
+ function pickDraw(markets) {
3204
+ return markets.find(isDrawMarket);
3205
+ }
3206
+ function assertAllowedHost(base) {
3207
+ let host;
3208
+ try {
3209
+ host = new URL(base).host;
3210
+ } catch {
3211
+ throw new Error(`Invalid Polymarket base URL: ${base}`);
3212
+ }
3213
+ if (!ALLOWED_HOSTS.has(host)) {
3214
+ throw new Error(`Polymarket host not allow-listed: ${host}`);
3215
+ }
3216
+ }
3217
+ function yesPrice(market) {
3218
+ const labels = parseJsonArray(market.outcomes);
3219
+ const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
3220
+ if (labels.length === 0 || labels.length !== prices.length) return void 0;
3221
+ const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
3222
+ if (i < 0) return void 0;
3223
+ const p = prices[i];
3224
+ return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
3225
+ }
3226
+ function parseJsonArray(v) {
3227
+ if (Array.isArray(v)) return v.map((x) => String(x));
3228
+ if (typeof v === "string") {
3229
+ try {
3230
+ const parsed = JSON.parse(v);
3231
+ return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
3232
+ } catch {
3233
+ return [];
3234
+ }
3235
+ }
3236
+ return [];
3237
+ }
3238
+ function numberish(v) {
3239
+ if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
3240
+ if (typeof v === "string") {
3241
+ const n = Number(v);
3242
+ return Number.isFinite(n) ? n : void 0;
3243
+ }
3244
+ return void 0;
3245
+ }
3246
+ function resolveMarketSource(explicit) {
3247
+ if (explicit) return explicit;
3248
+ if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
3249
+ return process.env.CLAUDINHO_MARKETS_SOURCE;
3250
+ }
3251
+ return "polymarket";
3252
+ }
3253
+ function makeMarketProvider(source) {
3254
+ switch (resolveMarketSource(source)) {
3255
+ case "fake":
3256
+ return new FakeMarketProvider({ synthesize: true });
3257
+ case "none":
3258
+ case "off":
3259
+ return new FakeMarketProvider();
3260
+ // no synth → yields no signals, no network
3261
+ default:
3262
+ return new PolymarketProvider();
3263
+ }
3264
+ }
3265
+ async function getMarketSignals(provider, matches, options) {
3266
+ try {
3267
+ return await provider.findSignals(matches, options);
3268
+ } catch {
3269
+ return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
3270
+ }
3271
+ }
2854
3272
 
2855
3273
  // src/config.ts
2856
3274
  var SUPPORTED_LANGS = ["en", "es", "pt", "fr"];
@@ -2876,6 +3294,11 @@ function pickColor(explicit) {
2876
3294
  function isSupportedLang(s) {
2877
3295
  return SUPPORTED_LANGS.includes(s);
2878
3296
  }
3297
+ function pickMarkets(explicit) {
3298
+ if (explicit === false) return false;
3299
+ if ((process.env.CLAUDINHO_MARKETS ?? "").toLowerCase() === "off") return false;
3300
+ return true;
3301
+ }
2879
3302
  function resolveConfig(opts) {
2880
3303
  const langRequestedUnsupported = opts.lang && !isSupportedLang(opts.lang) ? opts.lang : void 0;
2881
3304
  return {
@@ -2885,6 +3308,7 @@ function resolveConfig(opts) {
2885
3308
  color: pickColor(opts.color),
2886
3309
  source: opts.source ?? process.env.CLAUDINHO_SOURCE ?? "espn",
2887
3310
  flavor: asFlavorLevel(opts.flavor ?? process.env.CLAUDINHO_FLAVOR),
3311
+ markets: pickMarkets(opts.markets),
2888
3312
  langRequestedUnsupported
2889
3313
  };
2890
3314
  }
@@ -3090,34 +3514,91 @@ function header(text, c) {
3090
3514
  function disclaimer(t, c) {
3091
3515
  return c.dim(t("disclaimer"));
3092
3516
  }
3517
+ function dataSource(source, c) {
3518
+ return source ? c.dim(`Live data: ${liveSourceLabel(source)}`) : "";
3519
+ }
3520
+
3521
+ // src/marketCache.ts
3522
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
3523
+ import { homedir } from "os";
3524
+ import { join } from "path";
3525
+ var POSITIVE_TTL_MS = 10 * 6e4;
3526
+ var NEGATIVE_TTL_MS = 3 * 6e4;
3527
+ function cacheDir() {
3528
+ const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
3529
+ return join(base, "claudinho");
3530
+ }
3531
+ function cachePath() {
3532
+ return join(cacheDir(), "market-signals.json");
3533
+ }
3534
+ function readFile() {
3535
+ try {
3536
+ return JSON.parse(readFileSync(cachePath(), "utf8"));
3537
+ } catch {
3538
+ return void 0;
3539
+ }
3540
+ }
3541
+ function readMarketCache(source, competition, now = Date.now()) {
3542
+ const signals = /* @__PURE__ */ new Map();
3543
+ const checked = /* @__PURE__ */ new Set();
3544
+ const file = readFile();
3545
+ if (!file || file.source !== source || file.competition !== competition) {
3546
+ return { signals, checked };
3547
+ }
3548
+ for (const [id, entry] of Object.entries(file.entries ?? {})) {
3549
+ const t = Date.parse(entry.fetchedAt);
3550
+ if (!Number.isFinite(t)) continue;
3551
+ const ttl = entry.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
3552
+ if (now - t > ttl) continue;
3553
+ checked.add(id);
3554
+ if (entry.signal) signals.set(id, entry.signal);
3555
+ }
3556
+ return { signals, checked };
3557
+ }
3558
+ function writeMarketCache(source, competition, attempted, fetched, now = Date.now()) {
3559
+ if (attempted.length === 0) return;
3560
+ try {
3561
+ const existing = readFile();
3562
+ const base = existing && existing.source === source && existing.competition === competition ? existing : { source, competition, entries: {} };
3563
+ const fetchedAt = new Date(now).toISOString();
3564
+ for (const id of attempted) {
3565
+ base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
3566
+ }
3567
+ mkdirSync(cacheDir(), { recursive: true });
3568
+ const tmp = join(cacheDir(), `market-signals.${process.pid}.tmp`);
3569
+ writeFileSync(tmp, JSON.stringify(base));
3570
+ renameSync(tmp, cachePath());
3571
+ } catch {
3572
+ }
3573
+ }
3093
3574
 
3094
3575
  // src/cache.ts
3095
3576
  import {
3096
3577
  closeSync,
3097
- mkdirSync,
3578
+ mkdirSync as mkdirSync2,
3098
3579
  openSync,
3099
- readFileSync,
3100
- renameSync,
3580
+ readFileSync as readFileSync2,
3581
+ renameSync as renameSync2,
3101
3582
  rmSync,
3102
3583
  statSync,
3103
3584
  writeSync
3104
3585
  } from "fs";
3105
- import { homedir } from "os";
3106
- import { join } from "path";
3586
+ import { homedir as homedir2 } from "os";
3587
+ import { join as join2 } from "path";
3107
3588
  var LOCK_STALE_MS = 6e4;
3108
- function cacheDir() {
3109
- const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
3110
- return join(base, "claudinho");
3589
+ function cacheDir2() {
3590
+ const base = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
3591
+ return join2(base, "claudinho");
3111
3592
  }
3112
- function cachePath() {
3113
- return join(cacheDir(), "state.json");
3593
+ function cachePath2() {
3594
+ return join2(cacheDir2(), "state.json");
3114
3595
  }
3115
3596
  function lockPath() {
3116
- return join(cacheDir(), "refresh.lock");
3597
+ return join2(cacheDir2(), "refresh.lock");
3117
3598
  }
3118
3599
  function readState() {
3119
3600
  try {
3120
- return JSON.parse(readFileSync(cachePath(), "utf8"));
3601
+ return JSON.parse(readFileSync2(cachePath2(), "utf8"));
3121
3602
  } catch {
3122
3603
  return void 0;
3123
3604
  }
@@ -3127,11 +3608,11 @@ function readCurrentState(source, competition) {
3127
3608
  return s && s.source === source && s.competition === competition ? s : void 0;
3128
3609
  }
3129
3610
  function writeState(state) {
3130
- const dir = cacheDir();
3131
- mkdirSync(dir, { recursive: true });
3132
- const tmp = join(dir, `state.${process.pid}.tmp`);
3611
+ const dir = cacheDir2();
3612
+ mkdirSync2(dir, { recursive: true });
3613
+ const tmp = join2(dir, `state.${process.pid}.tmp`);
3133
3614
  writeFileSync_(tmp, JSON.stringify(state));
3134
- renameSync(tmp, cachePath());
3615
+ renameSync2(tmp, cachePath2());
3135
3616
  }
3136
3617
  function writeFileSync_(path, data) {
3137
3618
  const fd = openSync(path, "w");
@@ -3149,7 +3630,7 @@ function ageMs(state, now = Date.now()) {
3149
3630
  function lockAgeMs(now = Date.now()) {
3150
3631
  const lp = lockPath();
3151
3632
  try {
3152
- const contents = readFileSync(lp, "utf8");
3633
+ const contents = readFileSync2(lp, "utf8");
3153
3634
  const written = Number.parseInt(contents.split(/\s+/)[1] ?? "", 10);
3154
3635
  if (Number.isFinite(written)) return now - written;
3155
3636
  } catch {
@@ -3165,7 +3646,7 @@ function isLockFresh(now = Date.now()) {
3165
3646
  return lockAgeMs(now) < LOCK_STALE_MS;
3166
3647
  }
3167
3648
  function acquireLock(now = Date.now()) {
3168
- mkdirSync(cacheDir(), { recursive: true });
3649
+ mkdirSync2(cacheDir2(), { recursive: true });
3169
3650
  const lp = lockPath();
3170
3651
  try {
3171
3652
  const fd = openSync(lp, "wx");
@@ -3337,14 +3818,14 @@ function spawnRefresh(source) {
3337
3818
  import {
3338
3819
  copyFileSync,
3339
3820
  existsSync,
3340
- mkdirSync as mkdirSync2,
3341
- readFileSync as readFileSync2,
3342
- writeFileSync
3821
+ mkdirSync as mkdirSync3,
3822
+ readFileSync as readFileSync3,
3823
+ writeFileSync as writeFileSync2
3343
3824
  } from "fs";
3344
- import { homedir as homedir2 } from "os";
3345
- import { dirname, join as join2 } from "path";
3825
+ import { homedir as homedir3 } from "os";
3826
+ import { dirname, join as join3 } from "path";
3346
3827
  function claudeSettingsPath() {
3347
- return join2(homedir2(), ".claude", "settings.json");
3828
+ return join3(homedir3(), ".claude", "settings.json");
3348
3829
  }
3349
3830
  function backupOnce(path) {
3350
3831
  const bak = `${path}.claudinho.bak`;
@@ -3360,7 +3841,7 @@ function initStatusline(opts = {}) {
3360
3841
  let settings = {};
3361
3842
  if (existsSync(path)) {
3362
3843
  try {
3363
- settings = JSON.parse(readFileSync2(path, "utf8"));
3844
+ settings = JSON.parse(readFileSync3(path, "utf8"));
3364
3845
  } catch {
3365
3846
  return {
3366
3847
  action: "manual",
@@ -3376,8 +3857,8 @@ ${snippet}`
3376
3857
  }
3377
3858
  backupOnce(path);
3378
3859
  settings.statusLine = sl;
3379
- mkdirSync2(dirname(path), { recursive: true });
3380
- writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
3860
+ mkdirSync3(dirname(path), { recursive: true });
3861
+ writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
3381
3862
  return {
3382
3863
  action: "written",
3383
3864
  path,
@@ -3398,7 +3879,7 @@ function initHook(opts = {}) {
3398
3879
  let settings = {};
3399
3880
  if (existsSync(path)) {
3400
3881
  try {
3401
- settings = JSON.parse(readFileSync2(path, "utf8"));
3882
+ settings = JSON.parse(readFileSync3(path, "utf8"));
3402
3883
  } catch {
3403
3884
  return {
3404
3885
  action: "manual",
@@ -3420,8 +3901,8 @@ ${snippet}`
3420
3901
  }
3421
3902
  backupOnce(path);
3422
3903
  matchers.push({ hooks: [{ type: "command", command }] });
3423
- mkdirSync2(dirname(path), { recursive: true });
3424
- writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
3904
+ mkdirSync3(dirname(path), { recursive: true });
3905
+ writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
3425
3906
  return {
3426
3907
  action: "written",
3427
3908
  path,
@@ -3433,6 +3914,45 @@ ${snippet}`
3433
3914
  function adapterFor({ cfg, adapter }) {
3434
3915
  return adapter ?? makeAdapter(cfg.source);
3435
3916
  }
3917
+ var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
3918
+ var MARKETS_CMD_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
3919
+ async function marketSignalsFor(ctx, matches, opts = {}) {
3920
+ if (ctx.marketProvider) return (await getMarketSignals(ctx.marketProvider, matches, opts)).signals;
3921
+ const source = resolveMarketSource();
3922
+ if (source !== "polymarket") {
3923
+ return (await getMarketSignals(makeMarketProvider(source), matches, opts)).signals;
3924
+ }
3925
+ const competition = resolveCompetition();
3926
+ const { signals: cached, checked: cachedIds } = readMarketCache("polymarket", competition);
3927
+ const result = /* @__PURE__ */ new Map();
3928
+ const miss = [];
3929
+ for (const m of matches) {
3930
+ const hit = cached.get(m.id);
3931
+ if (hit) result.set(m.id, hit);
3932
+ else if (!cachedIds.has(m.id)) miss.push(m);
3933
+ }
3934
+ if (miss.length > 0) {
3935
+ const { signals: fetched, checked } = await getMarketSignals(
3936
+ makeMarketProvider("polymarket"),
3937
+ miss,
3938
+ opts
3939
+ );
3940
+ writeMarketCache("polymarket", competition, [...checked], fetched);
3941
+ for (const [id, s] of fetched) result.set(id, s);
3942
+ }
3943
+ return result;
3944
+ }
3945
+ async function reliableMarketSignals(ctx, matches) {
3946
+ if (ctx.cfg.markets === false) return /* @__PURE__ */ new Map();
3947
+ const raw = await marketSignalsFor(ctx, matches, DEFAULT_ON_MARKET_OPTS);
3948
+ const now = /* @__PURE__ */ new Date();
3949
+ const out2 = /* @__PURE__ */ new Map();
3950
+ for (const [id, s] of raw) if (isReliableMarketSignal(s, { now })) out2.set(id, s);
3951
+ return out2;
3952
+ }
3953
+ async function reliableMarketSignalFor(ctx, match) {
3954
+ return (await reliableMarketSignals(ctx, [match])).get(match.id);
3955
+ }
3436
3956
  function out(line2 = "") {
3437
3957
  process.stdout.write(line2 + "\n");
3438
3958
  }
@@ -3457,10 +3977,17 @@ async function cmdToday(date, ctx) {
3457
3977
  precheck(cfg, t, date);
3458
3978
  const adapter = adapterFor(ctx);
3459
3979
  const targetDate = date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
3460
- const { matches, degraded } = await getMatchesForDate(adapter, targetDate);
3980
+ const { matches, degraded, source } = await getMatchesForDate(adapter, targetDate);
3461
3981
  const todays = fixturesByDate(targetDate, matches, cfg.tz);
3982
+ const signals = await reliableMarketSignals(ctx, todays);
3462
3983
  if (cfg.json) {
3463
- emitJson({ date: targetDate, degraded, matches: todays });
3984
+ emitJson({
3985
+ date: targetDate,
3986
+ degraded,
3987
+ source: source ?? null,
3988
+ matches: todays,
3989
+ marketSignals: Object.fromEntries(signals)
3990
+ });
3464
3991
  return;
3465
3992
  }
3466
3993
  const c = painterFor(cfg);
@@ -3471,18 +3998,24 @@ async function cmdToday(date, ctx) {
3471
3998
  if (todays.length === 0) {
3472
3999
  out(c.dim(" " + t("today.none")));
3473
4000
  } else {
3474
- for (const m of todays) out(matchLine(m, cfg, t, c));
4001
+ for (const m of todays) {
4002
+ out(matchLine(m, cfg, t, c));
4003
+ const s = signals.get(m.id);
4004
+ if (s) out(" " + c.dim(marketLine(s, m)));
4005
+ }
3475
4006
  }
3476
4007
  out();
4008
+ const src = dataSource(source, c);
4009
+ if (src) out(src);
3477
4010
  out(disclaimer(t, c));
3478
4011
  }
3479
4012
  async function cmdLive(ctx) {
3480
4013
  const { cfg, t } = ctx;
3481
4014
  precheck(cfg, t);
3482
4015
  const adapter = adapterFor(ctx);
3483
- const { matches, degraded } = await getLiveMatches(adapter);
4016
+ const { matches, degraded, source } = await getLiveMatches(adapter);
3484
4017
  if (cfg.json) {
3485
- emitJson({ degraded, matches });
4018
+ emitJson({ degraded, source: source ?? null, matches });
3486
4019
  return;
3487
4020
  }
3488
4021
  const c = painterFor(cfg);
@@ -3495,6 +4028,8 @@ async function cmdLive(ctx) {
3495
4028
  for (const m of matches) out(matchLine(m, cfg, t, c));
3496
4029
  }
3497
4030
  out();
4031
+ const src = dataSource(source, c);
4032
+ if (src) out(src);
3498
4033
  out(disclaimer(t, c));
3499
4034
  }
3500
4035
  async function cmdNext(team, { cfg, t }) {
@@ -3528,7 +4063,7 @@ async function cmdTable(group, ctx) {
3528
4063
  const { cfg, t } = ctx;
3529
4064
  precheck(cfg, t);
3530
4065
  const adapter = adapterFor(ctx);
3531
- const { matches } = await getMatchesForDate(
4066
+ const { matches, degraded, source } = await getMatchesForDate(
3532
4067
  adapter,
3533
4068
  localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz)
3534
4069
  );
@@ -3538,7 +4073,11 @@ async function cmdTable(group, ctx) {
3538
4073
  group: g,
3539
4074
  standings: computeStandings(fixturesByGroup(g, matches))
3540
4075
  }));
3541
- emitJson(group ? tables[0] ?? null : tables);
4076
+ emitJson({
4077
+ degraded,
4078
+ source: source ?? null,
4079
+ tables: group ? tables[0] ?? null : tables
4080
+ });
3542
4081
  return;
3543
4082
  }
3544
4083
  const c = painterFor(cfg);
@@ -3578,6 +4117,8 @@ async function cmdTable(group, ctx) {
3578
4117
  out(table.toString());
3579
4118
  }
3580
4119
  out();
4120
+ const src = dataSource(source, c);
4121
+ if (src) out(src);
3581
4122
  out(disclaimer(t, c));
3582
4123
  }
3583
4124
  function cmdPrompt({ cfg }) {
@@ -3633,15 +4174,18 @@ async function cmdMatch(id, ctx) {
3633
4174
  precheck(cfg, t);
3634
4175
  const adapter = adapterFor(ctx);
3635
4176
  let match = allFixtures().find((m) => m.id === id);
4177
+ let liveSource;
3636
4178
  try {
3637
4179
  if (match) {
3638
4180
  const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
3639
4181
  match = live.find((m) => m.id === id) ?? match;
4182
+ liveSource = adapter.name;
3640
4183
  }
3641
4184
  } catch {
3642
4185
  }
4186
+ const marketSignal = match ? await reliableMarketSignalFor(ctx, match) : void 0;
3643
4187
  if (cfg.json) {
3644
- emitJson({ match: match ?? null });
4188
+ emitJson({ match: match ?? null, source: liveSource ?? null, marketSignal: marketSignal ?? null });
3645
4189
  return;
3646
4190
  }
3647
4191
  const c = painterFor(cfg);
@@ -3668,8 +4212,112 @@ async function cmdMatch(id, ctx) {
3668
4212
  out(` ${e.minute}' ${e.type} ${e.teamCode}${e.player ? ` \u2014 ${e.player}` : ""}`);
3669
4213
  }
3670
4214
  }
4215
+ if (marketSignal) {
4216
+ out();
4217
+ for (const mline of marketBlock(marketSignal, match)) out(" " + c.dim(mline));
4218
+ }
4219
+ out();
4220
+ const src = dataSource(liveSource, c);
4221
+ if (src) out(src);
4222
+ out(disclaimer(t, c));
4223
+ }
4224
+ var MARKET_INFO = "Prediction-market data is informational only.";
4225
+ function marketDisplayable(sig) {
4226
+ return !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
4227
+ }
4228
+ function marketHeaderLine(m) {
4229
+ return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag}`;
4230
+ }
4231
+ function printMarketBlock(m, sig, c) {
4232
+ for (const line2 of marketBlock(sig, m)) out(" " + c.dim(line2));
4233
+ }
4234
+ async function cmdMarkets(target, team, ctx) {
4235
+ const { cfg, t } = ctx;
4236
+ if (target === "next") {
4237
+ precheck(cfg, t);
4238
+ if (!team) throw new InputError("Usage: claudinho markets next <team>");
4239
+ const code = team.toUpperCase();
4240
+ const fixture = nextFixtureForTeam(code);
4241
+ const sig = fixture ? (await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS)).get(fixture.id) : void 0;
4242
+ const shown = sig && marketDisplayable(sig) ? sig : void 0;
4243
+ if (cfg.json) {
4244
+ emitJson({
4245
+ team: code,
4246
+ matchId: fixture?.id ?? null,
4247
+ informationalOnly: true,
4248
+ signal: shown ?? null
4249
+ });
4250
+ return;
4251
+ }
4252
+ const c2 = painterFor(cfg);
4253
+ out();
4254
+ if (!fixture) {
4255
+ out(c2.dim(" " + t("next.none", { team: code })));
4256
+ } else {
4257
+ out(header(marketHeaderLine(fixture), c2));
4258
+ out();
4259
+ if (shown) printMarketBlock(fixture, shown, c2);
4260
+ else out(c2.dim(" No market signal for this match."));
4261
+ }
4262
+ out();
4263
+ out(disclaimer(t, c2));
4264
+ out(c2.dim(MARKET_INFO));
4265
+ return;
4266
+ }
4267
+ if (target && target !== "today" && !isValidDate(target)) {
4268
+ precheck(cfg, t);
4269
+ const match = allFixtures().find((m) => m.id === target);
4270
+ const sig = match ? (await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS)).get(match.id) : void 0;
4271
+ const shown = sig && marketDisplayable(sig) ? sig : void 0;
4272
+ if (cfg.json) {
4273
+ emitJson({ matchId: target, informationalOnly: true, signal: shown ?? null });
4274
+ return;
4275
+ }
4276
+ const c2 = painterFor(cfg);
4277
+ out();
4278
+ if (!match) {
4279
+ out(c2.dim(" " + t("match.none", { id: target })));
4280
+ } else {
4281
+ out(header(marketHeaderLine(match), c2));
4282
+ out();
4283
+ if (shown) printMarketBlock(match, shown, c2);
4284
+ else out(c2.dim(" No market signal for this match."));
4285
+ }
4286
+ out();
4287
+ out(disclaimer(t, c2));
4288
+ out(c2.dim(MARKET_INFO));
4289
+ return;
4290
+ }
4291
+ const explicitDate = target && target !== "today" ? target : void 0;
4292
+ precheck(cfg, t, explicitDate);
4293
+ const date = explicitDate ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
4294
+ const { matches } = await getMatchesForDate(adapterFor(ctx), date);
4295
+ const todays = fixturesByDate(date, matches, cfg.tz);
4296
+ const signals = await marketSignalsFor(ctx, todays, MARKETS_CMD_OPTS);
4297
+ const rows = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
4298
+ (r) => !!r.signal && marketDisplayable(r.signal)
4299
+ );
4300
+ if (cfg.json) {
4301
+ const marketSignals = {};
4302
+ for (const r of rows) marketSignals[r.match.id] = r.signal;
4303
+ emitJson({ date, informationalOnly: true, marketSignals });
4304
+ return;
4305
+ }
4306
+ const c = painterFor(cfg);
4307
+ out();
4308
+ out(header(`Market signals \xB7 ${date}`, c));
3671
4309
  out();
4310
+ if (rows.length === 0) {
4311
+ out(c.dim(` No market signals available for ${date}.`));
4312
+ } else {
4313
+ for (const { match, signal } of rows) {
4314
+ out(" " + c.bold(marketHeaderLine(match)));
4315
+ printMarketBlock(match, signal, c);
4316
+ out();
4317
+ }
4318
+ }
3672
4319
  out(disclaimer(t, c));
4320
+ out(c.dim(MARKET_INFO));
3673
4321
  }
3674
4322
  var VIBES = [
3675
4323
  "Shipping code, watching goals.",
@@ -3704,7 +4352,7 @@ function handlePipeError(stream) {
3704
4352
  }
3705
4353
  handlePipeError(process.stdout);
3706
4354
  handlePipeError(process.stderr);
3707
- var VERSION = "0.2.0";
4355
+ var VERSION = "0.3.0";
3708
4356
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
3709
4357
  function ctxFrom(cmd) {
3710
4358
  const root = cmd.parent ?? cmd;
@@ -3719,7 +4367,7 @@ function fail(err) {
3719
4367
  process.exit(1);
3720
4368
  }
3721
4369
  var program = new Command();
3722
- program.name("claudinho").description("The 2026 football tournament in your terminal.\n" + DISCLAIMER).version(VERSION, "-v, --version").option("--lang <code>", "language: en, es, pt, fr").option("--tz <zone>", "IANA timezone, e.g. America/Mexico_City").option("--json", "output JSON (for scripting)").option("--no-color", "disable ANSI colors").option("--source <name>", "live data provider (advanced)").option("--flavor <level>", "commentary flair: off, subtle, full (default: full)");
4370
+ program.name("claudinho").description("The 2026 football tournament in your terminal.\n" + DISCLAIMER).version(VERSION, "-v, --version").option("--lang <code>", "language: en, es, pt, fr").option("--tz <zone>", "IANA timezone, e.g. America/Mexico_City").option("--json", "output JSON (for scripting)").option("--no-color", "disable ANSI colors").option("--source <name>", "live data provider (advanced)").option("--flavor <level>", "commentary flair: off, subtle, full (default: full)").option("--no-markets", "hide prediction-market signals (informational odds)");
3723
4371
  program.addHelpText("after", "\n#VibingLaVidaLoca \u26BD");
3724
4372
  program.command("today").description("show a day's fixtures (default: today)").argument("[date]", "date as YYYY-MM-DD").action(async (date, _opts, cmd) => {
3725
4373
  try {
@@ -3756,6 +4404,13 @@ program.command("match").description("show a single match by id").argument("<id>
3756
4404
  fail(e);
3757
4405
  }
3758
4406
  });
4407
+ program.command("markets").description("show prediction-market signals (read-only, informational only)").argument("[target]", 'date (YYYY-MM-DD), match id, "today", or "next"').argument("[team]", 'team code when target is "next" (e.g. MEX)').action(async (target, team, _opts, cmd) => {
4408
+ try {
4409
+ await cmdMarkets(target, team, ctxFrom(cmd));
4410
+ } catch (e) {
4411
+ fail(e);
4412
+ }
4413
+ });
3759
4414
  program.command("prompt").description("print a one-line status (Claude Code statusline, tmux, Starship, \u2026)").action((_opts, cmd) => {
3760
4415
  cmdPrompt(ctxFrom(cmd));
3761
4416
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.2.0",
4
- "description": "Claudinho CLI — the 2026 football tournament in your terminal. Live scores, fixtures, group tables. Not affiliated with FIFA or Anthropic.",
3
+ "version": "0.3.0",
4
+ "description": "Claudinho CLI — the 2026 men's football tournament in your terminal: live scores, fixtures, group tables, prediction-market odds. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Arturo Garrido",
@@ -48,7 +48,7 @@
48
48
  "tsup": "^8.0.0",
49
49
  "typescript": "^5.7.0",
50
50
  "vitest": "^4.1.0",
51
- "@claudinho/core": "0.2.0"
51
+ "@claudinho/core": "0.3.0"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsup",