@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.
- package/README.md +35 -3
- package/dist/index.js +697 -42
- 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,
|
|
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.
|
|
115
|
-
|
|
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
|
|
3109
|
-
const base = process.env.XDG_CACHE_HOME ||
|
|
3110
|
-
return
|
|
3589
|
+
function cacheDir2() {
|
|
3590
|
+
const base = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
|
|
3591
|
+
return join2(base, "claudinho");
|
|
3111
3592
|
}
|
|
3112
|
-
function
|
|
3113
|
-
return
|
|
3593
|
+
function cachePath2() {
|
|
3594
|
+
return join2(cacheDir2(), "state.json");
|
|
3114
3595
|
}
|
|
3115
3596
|
function lockPath() {
|
|
3116
|
-
return
|
|
3597
|
+
return join2(cacheDir2(), "refresh.lock");
|
|
3117
3598
|
}
|
|
3118
3599
|
function readState() {
|
|
3119
3600
|
try {
|
|
3120
|
-
return JSON.parse(
|
|
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 =
|
|
3131
|
-
|
|
3132
|
-
const 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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3341
|
-
readFileSync as
|
|
3342
|
-
writeFileSync
|
|
3821
|
+
mkdirSync as mkdirSync3,
|
|
3822
|
+
readFileSync as readFileSync3,
|
|
3823
|
+
writeFileSync as writeFileSync2
|
|
3343
3824
|
} from "fs";
|
|
3344
|
-
import { homedir as
|
|
3345
|
-
import { dirname, join as
|
|
3825
|
+
import { homedir as homedir3 } from "os";
|
|
3826
|
+
import { dirname, join as join3 } from "path";
|
|
3346
3827
|
function claudeSettingsPath() {
|
|
3347
|
-
return
|
|
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(
|
|
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
|
-
|
|
3380
|
-
|
|
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(
|
|
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
|
-
|
|
3424
|
-
|
|
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({
|
|
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)
|
|
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(
|
|
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.
|
|
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.
|
|
4
|
-
"description": "Claudinho CLI — the 2026 football tournament in your terminal
|
|
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.
|
|
51
|
+
"@claudinho/core": "0.3.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "tsup",
|