@claudinho/mcp 0.11.1 → 0.12.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 +117 -112
- package/dist/index.js +356 -88
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,149 +1,154 @@
|
|
|
1
1
|
# @claudinho/mcp ⚽
|
|
2
2
|
|
|
3
|
-
**An MCP server
|
|
4
|
-
the competition you follow: the World Cup, the Premier
|
|
5
|
-
12 more.**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
read-only Polymarket signals for the World Cup) hit the network. It ran the 2026 World Cup from
|
|
9
|
-
the opener to the final.
|
|
3
|
+
**An MCP server that lets your coding agent answer football questions: live scores, fixtures,
|
|
4
|
+
standings and paste-ready match cards for the competition you follow: the World Cup, the Premier
|
|
5
|
+
League, LALIGA, the Champions League and 12 more.** It works in Claude Code, Cursor, Codex, Claude
|
|
6
|
+
Desktop, Windsurf, Zed, VS Code and any MCP client that runs a local (stdio) server. No API key, no
|
|
7
|
+
signup, no account.
|
|
10
8
|
|
|
11
9
|
<p align="center">
|
|
12
|
-
<img src="https://raw.githubusercontent.com/arturogarrido/claudinho/main/.github/assets/mcp-aha.gif" alt="
|
|
10
|
+
<img src="https://raw.githubusercontent.com/arturogarrido/claudinho/main/.github/assets/mcp-aha.gif" alt="Cursor CLI asked mid-task how Manchester City are doing against Paris Saint-Germain; it calls the Claudinho MCP server and answers with the live score from the 24th minute" width="760">
|
|
13
11
|
</p>
|
|
14
12
|
|
|
15
|
-
> ⭐ Added via `npx` or your MCP config? **[Star the repo](https://github.com/arturogarrido/claudinho)**: a fan project runs on stars.
|
|
16
|
-
|
|
17
13
|
## Install
|
|
18
14
|
|
|
15
|
+
The server runs with `npx`, so it needs Node.js 20 or later.
|
|
16
|
+
|
|
19
17
|
**Claude Code**
|
|
20
18
|
```bash
|
|
21
19
|
claude mcp add claudinho -- npx -y @claudinho/mcp
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
**
|
|
22
|
+
**Codex CLI**
|
|
23
|
+
```bash
|
|
24
|
+
codex mcp add claudinho -- npx -y @claudinho/mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Cursor, Claude Desktop, Windsurf, Zed, VS Code**: add this to the client's MCP config (for
|
|
28
|
+
Cursor, `~/.cursor/mcp.json` or a project's `.cursor/mcp.json`; for Claude Desktop, Settings →
|
|
29
|
+
Developer → Edit Config, then restart). Some clients (VS Code, Zed) put the same `command` and
|
|
30
|
+
`args` under a key of their own: see your client's MCP docs.
|
|
25
31
|
```json
|
|
26
32
|
{ "mcpServers": { "claudinho": { "command": "npx", "args": ["-y", "@claudinho/mcp"] } } }
|
|
27
33
|
```
|
|
28
|
-
> Bonus: `claudinho init-cursor-statusline` (from `@claudinho/cli`) also puts the live score
|
|
29
|
-
> in your Cursor CLI statusline, or run `claudinho init cursor` to do both at once.
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
Using Cursor CLI too? `claudinho init cursor` (from `@claudinho/cli`) puts the live score in its
|
|
36
|
+
statusline and prints this config for you to paste.
|
|
37
|
+
|
|
38
|
+
## Ask your first question
|
|
39
|
+
|
|
40
|
+
The server answers for one competition at a time. Either name it in your question:
|
|
41
|
+
|
|
42
|
+
> "What Champions League matches are on today?" · "Show me the Premier League table" ·
|
|
43
|
+
> "When does Arsenal play next in the Champions League?"
|
|
44
|
+
|
|
45
|
+
or choose it once with the CLI, and then just ask:
|
|
46
|
+
|
|
32
47
|
```bash
|
|
33
|
-
|
|
48
|
+
npx @claudinho/cli follow champions-league --team Arsenal
|
|
34
49
|
```
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`~/.codex/config.toml` (`[mcp_servers.claudinho]`, `command = "npx"`, `args = ["-y", "@claudinho/mcp"]`).
|
|
39
|
-
|
|
40
|
-
Then just ask your agent naturally; it picks the right tool and answers with live data:
|
|
51
|
+
> "What matches are on today?" · "Show me the live scores" · "When's our next game?" ·
|
|
52
|
+
> "Make me a shareable card for today"
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
> "Show the Premier League table" · "Show the World Cup bracket" · "Make me a shareable card for today"
|
|
54
|
+
Without a competition, the tools that answer for one say how to choose it.
|
|
44
55
|
|
|
45
56
|
## Tools
|
|
46
57
|
|
|
58
|
+
Every tool is read-only, and two of them never touch the network.
|
|
59
|
+
|
|
47
60
|
| Tool | What it does |
|
|
48
61
|
|---|---|
|
|
49
|
-
| `get_today` | fixtures for a date (default: today)
|
|
50
|
-
| `get_live` | matches in play right now |
|
|
51
|
-
| `get_match` |
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `get_team` |
|
|
58
|
-
| `list_competitions` | the supported competitions
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(`en
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
the cries) in the server `env`, or per call via the `flavor` argument.
|
|
62
|
+
| `get_today` | the fixtures for a date (default: today) in the caller's timezone, with live scores |
|
|
63
|
+
| `get_live` | the matches in play right now |
|
|
64
|
+
| `get_match` | one match by id, with the goals and cards the provider sent for a match in play or finished |
|
|
65
|
+
| `get_next_fixture` | a team's next match, by name or code (`Arsenal`, `ARS`, `Mexico`, `MEX`); with no `team`, the server's `CLAUDINHO_TEAM`, else the team the user pinned (`claudinho follow <alias> --team <name>`) when the request is for its competition |
|
|
66
|
+
| `get_standings` | the standings: every table, or one by its key (a group letter, or `A1`, `A-B`, `LEAGUE`) |
|
|
67
|
+
| `get_bracket` | the knockout bracket, World Cup only, from the Round of 32 to the final; an optional `stage` (`R32`, `R16`, `QF`, `SF`, `3P`, `F`) keeps one round |
|
|
68
|
+
| `get_share_snippet` | a plain-text card for a match, a team's next match, a table, the bracket (`bracket: true`, with an optional `knockoutStage`), a date or live; hand it to the user as it is |
|
|
69
|
+
| `get_market_signal` | World Cup prediction-market odds for a match (`matchId`), a team's current or next match (`team`, a 3-letter code such as `MEX`), or a date; informational only |
|
|
70
|
+
| `get_team` | a World Cup nation's name or code to its FIFA code, flag and group (offline) |
|
|
71
|
+
| `list_competitions` | the supported competitions, the alias each takes and what each offers, and the one in effect (offline) |
|
|
72
|
+
|
|
73
|
+
Every tool but `get_team` takes an optional `competition` (an alias such as `champions-league`), and
|
|
74
|
+
most take `tz`, `lang` (`en`, `es`, `pt`, `fr`) and `flavor` (`off`, `subtle`, `full`).
|
|
75
|
+
|
|
76
|
+
Resources: `standings://{group}`, and `fixtures://{date}` (the bundled World Cup schedule).
|
|
77
|
+
Prompts: `my_team` (your team's next match and its standing, in the competition you follow) and
|
|
78
|
+
`tournament_today`.
|
|
79
|
+
|
|
80
|
+
## Prediction-market odds (World Cup)
|
|
81
|
+
|
|
82
|
+
For World Cup matches, `get_today`, `get_match` and a `get_share_snippet` card for a match, a
|
|
83
|
+
team's next match or a date add a short "who's favored" line when a reliable Polymarket market
|
|
84
|
+
exists, and `get_market_signal` reads one on its own. **Informational only, not betting advice:**
|
|
85
|
+
market-implied percentages with Polymarket's attribution, never links or trade calls. Finished
|
|
86
|
+
matches never show one. `CLAUDINHO_MARKETS=off` in the server's `env` turns the short line off;
|
|
87
|
+
`get_market_signal` still answers.
|
|
88
|
+
|
|
89
|
+
## Commentary
|
|
90
|
+
|
|
91
|
+
A match line in a tool's text ends with a short line of commentary in the reader's language
|
|
92
|
+
(`— ¡GOOOOL!`), or a team's own rally cry when it has one (Mexico's `¿Y si sí?`, Pumas' `¡Goya!`,
|
|
93
|
+
Arsenal's `COYG!`). A postponed or cancelled match gets neither. The commentary never appears in
|
|
94
|
+
the structured data. Turn it down with `CLAUDINHO_FLAVOR=subtle` or `off` in the server's `env`, or
|
|
95
|
+
per call with `flavor`.
|
|
96
|
+
|
|
97
|
+
## Details for agent builders
|
|
98
|
+
|
|
99
|
+
**Choosing the competition.** A request's competition is its `competition` argument, else the
|
|
100
|
+
server's `CLAUDINHO_COMPETITION`, else the choice `claudinho follow <alias>` saved. An unknown value
|
|
101
|
+
is a tool error that lists the aliases. With none of the three, every competition-answering tool
|
|
102
|
+
answers `noCompetition: true` and `competition: null` beside its empty shape, with a sentence saying
|
|
103
|
+
what to do; `list_competitions` answers `current: null`.
|
|
104
|
+
|
|
105
|
+
**What every answer carries.** Human-readable text and structured content, validated against each
|
|
106
|
+
tool's declared `outputSchema`. The text starts with the competition it is for (a share card says it
|
|
107
|
+
in its title), and the structured content carries it as `competition`. Two tools differ:
|
|
108
|
+
`get_team` reads the World Cup's nations whatever the competition and carries no `competition`, and
|
|
109
|
+
`list_competitions` names the one in effect last in its text and as `current` in its data.
|
|
110
|
+
|
|
111
|
+
**Answers that are not whole.** When the provider sent records that could not be read,
|
|
112
|
+
`get_today`, `get_live`, `get_match`, `get_next_fixture`, `get_bracket`, `get_market_signal` and
|
|
113
|
+
`get_share_snippet` say so before what they show and carry `partial` (with `omitted`, the count of
|
|
114
|
+
records left out, when it is known). An empty answer then means nothing was read, not that nothing
|
|
115
|
+
exists. In particular, a missing next match does not mean a team is out.
|
|
116
|
+
|
|
117
|
+
**Club competitions.** `get_next_fixture` and `get_match` search from yesterday to 14 days ahead:
|
|
118
|
+
an empty answer carrying `horizon` or `window` is about that span. Several teams matching one name
|
|
119
|
+
come back as `candidates`. Between two seasons, `get_today`, `get_live`, `get_next_fixture` and
|
|
120
|
+
`get_match` carry `betweenEditions`. `get_bracket` answers `inapplicable` for a league season with no
|
|
121
|
+
knockout tie (the Premier League, LALIGA), and `unsupported` for every other competition but the
|
|
122
|
+
World Cup. A club has no `flag`. A match's `stage` is `REGULAR` for a league's season, `LEAGUE` for
|
|
123
|
+
a cup's league phase, `PO` for play-offs, or `OTHER` with the provider's own words in `stageLabel`.
|
|
124
|
+
|
|
125
|
+
**Markets.** `get_today`, `get_match` and a `get_share_snippet` card for a match, a team's next
|
|
126
|
+
match, a date or live matches carry `marketComplete` (a table or bracket card does not), and
|
|
127
|
+
`get_market_signal` carries `complete`. `false` means the
|
|
128
|
+
optional market read did not finish, which is not the same as "no signal"; the text, and a share
|
|
129
|
+
card, say so too. `CLAUDINHO_MARKETS_SOURCE=fake` gives a clearly labelled, network-free preview.
|
|
130
|
+
|
|
131
|
+
**The process.** Stdout carries only the MCP protocol; diagnostics go to stderr.
|
|
132
|
+
`CLAUDINHO_DEBUG=1` in the server's `env` prints the data provider's failures there.
|
|
121
133
|
|
|
122
134
|
## How it works
|
|
123
135
|
|
|
124
|
-
The World Cup's 104 fixtures ship
|
|
125
|
-
|
|
126
|
-
from
|
|
127
|
-
signals from Polymarket. Stdout carries only the MCP protocol;
|
|
128
|
-
diagnostics go to stderr.
|
|
136
|
+
The World Cup's 104 fixtures ship with the package. Every other competition's schedule, and every
|
|
137
|
+
live score, comes from ESPN's public scoreboard (an answer from it names ESPN). World Cup market odds
|
|
138
|
+
come from Polymarket's public data. The server keeps its cache in memory only.
|
|
129
139
|
|
|
130
|
-
## Privacy
|
|
140
|
+
## Privacy
|
|
131
141
|
|
|
132
|
-
No
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
nothing to disk. Full policy: [PRIVACY.md](https://github.com/arturogarrido/claudinho/blob/main/PRIVACY.md).
|
|
142
|
+
No accounts, no telemetry, no analytics, no tracking, and no Claudinho server. The server asks ESPN
|
|
143
|
+
(and Polymarket, for World Cup odds) for public data with nothing about you attached; those services
|
|
144
|
+
still see standard request metadata, such as your IP address, like any web request. It writes
|
|
145
|
+
nothing to disk. Full policy:
|
|
146
|
+
[PRIVACY.md](https://github.com/arturogarrido/claudinho/blob/main/PRIVACY.md).
|
|
138
147
|
|
|
139
148
|
## License
|
|
140
149
|
|
|
141
|
-
MIT © 2026 Arturo Garrido · [source
|
|
142
|
-
|
|
143
|
-
> **Not affiliated with FIFA, any confederation, league or club, or Anthropic. Nor endorsed by or connected to any of them.** An independent,
|
|
144
|
-
> open-source fan project showing factual match data with emoji flags only: no logos, crests,
|
|
145
|
-
> kits, broadcast footage, or player likenesses.
|
|
150
|
+
MIT © 2026 Arturo Garrido · [source and issues](https://github.com/arturogarrido/claudinho)
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
Independent fan project. Not affiliated with FIFA, any confederation, league or club, or Anthropic.
|
|
148
153
|
|
|
149
154
|
_Built while watching the games._ **#VibingLaVidaLoca** ⚽
|
package/dist/index.js
CHANGED
|
@@ -342,6 +342,13 @@ var EN = {
|
|
|
342
342
|
"status.fullTime": "full-time",
|
|
343
343
|
"status.postponed": "postponed",
|
|
344
344
|
"status.cancelled": "cancelled",
|
|
345
|
+
// A timeline row's kind word (core `timelineRows`).
|
|
346
|
+
"event.goal": "Goal",
|
|
347
|
+
"event.ownGoal": "Own goal",
|
|
348
|
+
"event.penalty": "Penalty",
|
|
349
|
+
"event.yellow": "Yellow card",
|
|
350
|
+
"event.red": "Red card",
|
|
351
|
+
"event.sub": "Substitution",
|
|
345
352
|
"live.title": "Live now",
|
|
346
353
|
"today.title": "Today's matches",
|
|
347
354
|
"today.on": "Matches",
|
|
@@ -350,6 +357,23 @@ var EN = {
|
|
|
350
357
|
"next.in": "in {countdown}",
|
|
351
358
|
// A kickoff that has passed, where the countdown would say "in" (core `countdownPhrase`).
|
|
352
359
|
"countdown.now": "now",
|
|
360
|
+
// The ambient line's syncing phrase (`claudinho ambient --json` alone; the
|
|
361
|
+
// statusline's own `prompt` stays English).
|
|
362
|
+
"ambient.syncing": "live \xB7 syncing\u2026",
|
|
363
|
+
// The plugin's moment words, sent by `claudinho ambient --json` as `words`
|
|
364
|
+
// (`{team}` a template the plugin fills with the record's own names). Each
|
|
365
|
+
// at most 40 characters; `{team}` once in `moment.goalFor` and `moment.win`
|
|
366
|
+
// and nowhere else, or the plugin falls back to its English words.
|
|
367
|
+
"moment.goal": "GOAL!",
|
|
368
|
+
"moment.ownGoal": "Own goal",
|
|
369
|
+
"moment.ownGoalDetail": "own goal",
|
|
370
|
+
"moment.goalFor": "Goal for {team}",
|
|
371
|
+
"moment.goals": "Goals",
|
|
372
|
+
"moment.scoreUpdated": "Score updated",
|
|
373
|
+
"moment.penalties": "Penalties",
|
|
374
|
+
"moment.fullTime": "Full time",
|
|
375
|
+
"moment.win": "{team} win",
|
|
376
|
+
"moment.draw": "Draw",
|
|
353
377
|
// A title that introduces the lines below it (the MCP text).
|
|
354
378
|
heading: "{title}:",
|
|
355
379
|
// A list's empty state and the outage lines: the CLI's sentences, the MCP
|
|
@@ -361,6 +385,7 @@ var EN = {
|
|
|
361
385
|
"feed.earlierRecord": "Live state could not be refreshed \u2014 showing the provider's earlier record.",
|
|
362
386
|
"next.none": "No upcoming fixture found for {team}.",
|
|
363
387
|
"match.none": "No match found with id {id}.",
|
|
388
|
+
"match.kickoff": "Kickoff",
|
|
364
389
|
// The MCP text's own sentences for these states (the CLI says `live.degraded`
|
|
365
390
|
// there): they keep the team or the id the answer is about.
|
|
366
391
|
"next.unreachable": "Couldn't reach the data provider \u2014 no upcoming fixture confirmed for {team}.",
|
|
@@ -438,6 +463,12 @@ var ES = {
|
|
|
438
463
|
"status.fullTime": "finalizado",
|
|
439
464
|
"status.postponed": "aplazado",
|
|
440
465
|
"status.cancelled": "cancelado",
|
|
466
|
+
"event.goal": "Gol",
|
|
467
|
+
"event.ownGoal": "Gol en contra",
|
|
468
|
+
"event.penalty": "Penal",
|
|
469
|
+
"event.yellow": "Tarjeta amarilla",
|
|
470
|
+
"event.red": "Tarjeta roja",
|
|
471
|
+
"event.sub": "Cambio",
|
|
441
472
|
"live.title": "En vivo",
|
|
442
473
|
"today.title": "Partidos de hoy",
|
|
443
474
|
"today.on": "Partidos",
|
|
@@ -445,6 +476,17 @@ var ES = {
|
|
|
445
476
|
"next.label": "Pr\xF3ximo partido de {team}",
|
|
446
477
|
"next.in": "en {countdown}",
|
|
447
478
|
"countdown.now": "ahora",
|
|
479
|
+
"ambient.syncing": "en vivo \xB7 sincronizando\u2026",
|
|
480
|
+
"moment.goal": "\xA1GOL!",
|
|
481
|
+
"moment.ownGoal": "Gol en contra",
|
|
482
|
+
"moment.ownGoalDetail": "gol en contra",
|
|
483
|
+
"moment.goalFor": "Gol de {team}",
|
|
484
|
+
"moment.goals": "Goles",
|
|
485
|
+
"moment.scoreUpdated": "Marcador corregido",
|
|
486
|
+
"moment.penalties": "Penales",
|
|
487
|
+
"moment.fullTime": "Final",
|
|
488
|
+
"moment.win": "Gana {team}",
|
|
489
|
+
"moment.draw": "Empate",
|
|
448
490
|
heading: "{title}:",
|
|
449
491
|
"today.none": "No hay partidos para esta fecha.",
|
|
450
492
|
"live.none": "No hay partidos en juego ahora mismo.",
|
|
@@ -453,6 +495,7 @@ var ES = {
|
|
|
453
495
|
"feed.earlierRecord": "No se pudo actualizar el estado en vivo \u2014 se muestra el registro anterior del proveedor.",
|
|
454
496
|
"next.none": "No se encontr\xF3 pr\xF3ximo partido para {team}.",
|
|
455
497
|
"match.none": "No se encontr\xF3 partido con id {id}.",
|
|
498
|
+
"match.kickoff": "Inicio",
|
|
456
499
|
"next.unreachable": "No se pudo contactar al proveedor de datos \u2014 ning\xFAn pr\xF3ximo partido confirmado para {team}.",
|
|
457
500
|
"match.unreachable": "No se pudo contactar al proveedor de datos \u2014 no se pudo consultar el partido {id}.",
|
|
458
501
|
"team.ambiguous": '"{query}" es ambiguo. \xBFQuisiste decir:',
|
|
@@ -527,6 +570,12 @@ var PT = {
|
|
|
527
570
|
"status.fullTime": "encerrado",
|
|
528
571
|
"status.postponed": "adiado",
|
|
529
572
|
"status.cancelled": "cancelado",
|
|
573
|
+
"event.goal": "Gol",
|
|
574
|
+
"event.ownGoal": "Gol contra",
|
|
575
|
+
"event.penalty": "P\xEAnalti",
|
|
576
|
+
"event.yellow": "Cart\xE3o amarelo",
|
|
577
|
+
"event.red": "Cart\xE3o vermelho",
|
|
578
|
+
"event.sub": "Substitui\xE7\xE3o",
|
|
530
579
|
"live.title": "Ao vivo",
|
|
531
580
|
"today.title": "Jogos de hoje",
|
|
532
581
|
"today.on": "Jogos",
|
|
@@ -534,6 +583,17 @@ var PT = {
|
|
|
534
583
|
"next.label": "Pr\xF3ximo jogo de {team}",
|
|
535
584
|
"next.in": "em {countdown}",
|
|
536
585
|
"countdown.now": "agora",
|
|
586
|
+
"ambient.syncing": "ao vivo \xB7 sincronizando\u2026",
|
|
587
|
+
"moment.goal": "GOL!",
|
|
588
|
+
"moment.ownGoal": "Gol contra",
|
|
589
|
+
"moment.ownGoalDetail": "gol contra",
|
|
590
|
+
"moment.goalFor": "Gol \xB7 {team}",
|
|
591
|
+
"moment.goals": "Gols",
|
|
592
|
+
"moment.scoreUpdated": "Placar corrigido",
|
|
593
|
+
"moment.penalties": "P\xEAnaltis",
|
|
594
|
+
"moment.fullTime": "Fim de jogo",
|
|
595
|
+
"moment.win": "{team} vence",
|
|
596
|
+
"moment.draw": "Empate",
|
|
537
597
|
heading: "{title}:",
|
|
538
598
|
"today.none": "Nenhum jogo para esta data.",
|
|
539
599
|
"live.none": "Nenhum jogo em andamento agora.",
|
|
@@ -542,6 +602,7 @@ var PT = {
|
|
|
542
602
|
"feed.earlierRecord": "N\xE3o foi poss\xEDvel atualizar o estado ao vivo \u2014 mostrando o registro anterior do provedor.",
|
|
543
603
|
"next.none": "Nenhum pr\xF3ximo jogo encontrado para {team}.",
|
|
544
604
|
"match.none": "Nenhum jogo encontrado com id {id}.",
|
|
605
|
+
"match.kickoff": "In\xEDcio",
|
|
545
606
|
"next.unreachable": "N\xE3o foi poss\xEDvel contatar o provedor de dados \u2014 nenhum pr\xF3ximo jogo confirmado para {team}.",
|
|
546
607
|
"match.unreachable": "N\xE3o foi poss\xEDvel contatar o provedor de dados \u2014 o jogo {id} n\xE3o p\xF4de ser consultado.",
|
|
547
608
|
"team.ambiguous": '"{query}" \xE9 amb\xEDguo. Voc\xEA quis dizer:',
|
|
@@ -616,6 +677,12 @@ var FR = {
|
|
|
616
677
|
"status.fullTime": "termin\xE9",
|
|
617
678
|
"status.postponed": "report\xE9",
|
|
618
679
|
"status.cancelled": "annul\xE9",
|
|
680
|
+
"event.goal": "But",
|
|
681
|
+
"event.ownGoal": "But contre son camp",
|
|
682
|
+
"event.penalty": "Penalty",
|
|
683
|
+
"event.yellow": "Carton jaune",
|
|
684
|
+
"event.red": "Carton rouge",
|
|
685
|
+
"event.sub": "Remplacement",
|
|
619
686
|
"live.title": "En direct",
|
|
620
687
|
"today.title": "Matchs d'aujourd'hui",
|
|
621
688
|
"today.on": "Matchs",
|
|
@@ -623,6 +690,17 @@ var FR = {
|
|
|
623
690
|
"next.label": "Prochain match de {team}",
|
|
624
691
|
"next.in": "dans {countdown}",
|
|
625
692
|
"countdown.now": "maintenant",
|
|
693
|
+
"ambient.syncing": "en direct \xB7 synchronisation\u2026",
|
|
694
|
+
"moment.goal": "BUT !",
|
|
695
|
+
"moment.ownGoal": "But contre son camp",
|
|
696
|
+
"moment.ownGoalDetail": "contre son camp",
|
|
697
|
+
"moment.goalFor": "But pour {team}",
|
|
698
|
+
"moment.goals": "Buts",
|
|
699
|
+
"moment.scoreUpdated": "Score corrig\xE9",
|
|
700
|
+
"moment.penalties": "Tirs au but",
|
|
701
|
+
"moment.fullTime": "Fin du match",
|
|
702
|
+
"moment.win": "{team} gagne",
|
|
703
|
+
"moment.draw": "Match nul",
|
|
626
704
|
heading: "{title} :",
|
|
627
705
|
"today.none": "Aucun match pr\xE9vu pour cette date.",
|
|
628
706
|
"live.none": "Aucun match en cours pour l'instant.",
|
|
@@ -631,6 +709,7 @@ var FR = {
|
|
|
631
709
|
"feed.earlierRecord": "L\u2019\xE9tat en direct n\u2019a pas pu \xEAtre actualis\xE9 \u2014 affichage de l\u2019enregistrement pr\xE9c\xE9dent du fournisseur.",
|
|
632
710
|
"next.none": "Aucun prochain match trouv\xE9 pour {team}.",
|
|
633
711
|
"match.none": "Aucun match trouv\xE9 avec id {id}.",
|
|
712
|
+
"match.kickoff": "Coup d\u2019envoi",
|
|
634
713
|
"next.unreachable": "Impossible de joindre le fournisseur de donn\xE9es \u2014 aucun prochain match confirm\xE9 pour {team}.",
|
|
635
714
|
"match.unreachable": "Impossible de joindre le fournisseur de donn\xE9es \u2014 le match {id} n\u2019a pas pu \xEAtre consult\xE9.",
|
|
636
715
|
"team.ambiguous": '"{query}" est ambigu. Vouliez-vous dire :',
|
|
@@ -769,7 +848,7 @@ function formatTime(iso, opts = {}) {
|
|
|
769
848
|
}).format(when);
|
|
770
849
|
}
|
|
771
850
|
var COUNTDOWN_NOW = "now";
|
|
772
|
-
function countdown(iso, from
|
|
851
|
+
function countdown(iso, from) {
|
|
773
852
|
const when = parsedDate(iso);
|
|
774
853
|
if (!when) return UNKNOWN_TIME;
|
|
775
854
|
const ms = when.getTime() - from.getTime();
|
|
@@ -782,7 +861,7 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
|
|
|
782
861
|
if (hours > 0) return `${hours}h${mins}m`;
|
|
783
862
|
return `${mins}m`;
|
|
784
863
|
}
|
|
785
|
-
function countdownPhrase(lang, iso, from
|
|
864
|
+
function countdownPhrase(lang, iso, from) {
|
|
786
865
|
const left = countdown(iso, from);
|
|
787
866
|
return left === COUNTDOWN_NOW ? t(lang, "countdown.now") : t(lang, "next.in", { countdown: left });
|
|
788
867
|
}
|
|
@@ -832,6 +911,48 @@ function withFlag(text, flag2, side) {
|
|
|
832
911
|
if (typeof flag2 !== "string" || flag2 === "") return text;
|
|
833
912
|
return side === "home" ? `${flag2} ${text}` : `${text} ${flag2}`;
|
|
834
913
|
}
|
|
914
|
+
var KIND_KEYS = Object.freeze({
|
|
915
|
+
GOAL: "event.goal",
|
|
916
|
+
OWN_GOAL: "event.ownGoal",
|
|
917
|
+
PEN: "event.penalty",
|
|
918
|
+
YELLOW: "event.yellow",
|
|
919
|
+
RED: "event.red",
|
|
920
|
+
SUB: "event.sub"
|
|
921
|
+
});
|
|
922
|
+
var KIND_MARKS = Object.freeze({
|
|
923
|
+
GOAL: "\u26BD",
|
|
924
|
+
OWN_GOAL: "\u26BD",
|
|
925
|
+
PEN: "\u26BD",
|
|
926
|
+
YELLOW: "\u{1F7E8}",
|
|
927
|
+
RED: "\u{1F7E5}",
|
|
928
|
+
SUB: "\u{1F504}"
|
|
929
|
+
});
|
|
930
|
+
function timelineParts(match, opts) {
|
|
931
|
+
const events = match.events ?? [];
|
|
932
|
+
if (events.length === 0) return [];
|
|
933
|
+
const cells = events.map((e) => {
|
|
934
|
+
const team = e.side === "home" ? match.home : match.away;
|
|
935
|
+
return {
|
|
936
|
+
minute: e.added !== void 0 ? `${e.minute}+${e.added}'` : `${e.minute}'`,
|
|
937
|
+
mark: KIND_MARKS[e.type],
|
|
938
|
+
kind: t(opts.lang, KIND_KEYS[e.type]),
|
|
939
|
+
label: withFlag(team.code, opts.flags ? team.flag : void 0, e.side),
|
|
940
|
+
player: e.player
|
|
941
|
+
};
|
|
942
|
+
});
|
|
943
|
+
const widest = (pick2) => Math.max(...cells.map((c) => displayWidth(pick2(c))));
|
|
944
|
+
const minuteW = widest((c) => c.minute);
|
|
945
|
+
const kindW = widest((c) => c.kind);
|
|
946
|
+
const labelW = widest((c) => c.label);
|
|
947
|
+
return cells.map((c) => {
|
|
948
|
+
const columns = ` ${padVisible(c.minute, minuteW)} ${c.mark} ${padVisible(c.kind, kindW)} ${padVisible(c.label, labelW)}`;
|
|
949
|
+
if (!c.player) return [{ text: columns.trimEnd() }];
|
|
950
|
+
return [{ text: `${columns} ` }, { text: c.player, style: "strong" }];
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function timelineRows(match, opts) {
|
|
954
|
+
return timelineParts(match, opts).map((row) => row.map((p) => p.text).join(""));
|
|
955
|
+
}
|
|
835
956
|
function outcomeFromScore(home, away) {
|
|
836
957
|
if (home > away) return "H";
|
|
837
958
|
if (home < away) return "A";
|
|
@@ -1113,18 +1234,33 @@ function sealScorePair(raw) {
|
|
|
1113
1234
|
const away = count(v.away, MAX_GOALS);
|
|
1114
1235
|
return home !== void 0 && away !== void 0 ? { home, away } : void 0;
|
|
1115
1236
|
}
|
|
1237
|
+
var MAX_ADDED_MINUTES = 30;
|
|
1238
|
+
var SIDES = /* @__PURE__ */ new Set(["home", "away"]);
|
|
1116
1239
|
function sealEvent(raw) {
|
|
1117
|
-
if (!raw || typeof raw !== "object") return
|
|
1240
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1118
1241
|
const e = raw;
|
|
1119
1242
|
const type = member(e.type, EVENT_TYPES);
|
|
1120
|
-
if (!type) return
|
|
1243
|
+
if (!type) return null;
|
|
1121
1244
|
const minute = count(e.minute, MAX_MINUTE);
|
|
1122
|
-
if (minute === void 0) return
|
|
1123
|
-
|
|
1245
|
+
if (minute === void 0) return null;
|
|
1246
|
+
let added;
|
|
1247
|
+
if (e.added !== void 0) {
|
|
1248
|
+
added = count(e.added, MAX_ADDED_MINUTES);
|
|
1249
|
+
if (added === void 0 || added < 1) return null;
|
|
1250
|
+
}
|
|
1251
|
+
const side = member(e.side, SIDES);
|
|
1252
|
+
if (!side) return null;
|
|
1253
|
+
const out = { type, minute };
|
|
1254
|
+
if (added !== void 0) out.added = added;
|
|
1255
|
+
out.side = side;
|
|
1256
|
+
out.teamCode = humanLabel(e.teamCode, TEAM_CODE_COLUMNS);
|
|
1124
1257
|
const player = humanLabel(e.player);
|
|
1125
1258
|
if (player) out.player = player;
|
|
1126
1259
|
return out;
|
|
1127
1260
|
}
|
|
1261
|
+
function byClock(a, b) {
|
|
1262
|
+
return a.minute - b.minute || (a.added ?? 0) - (b.added ?? 0);
|
|
1263
|
+
}
|
|
1128
1264
|
function sealMatch(parts, opts = {}) {
|
|
1129
1265
|
if (!parts || typeof parts !== "object") return malformed("match is not an object");
|
|
1130
1266
|
const id = opaqueId(parts.id, ESPN_ID);
|
|
@@ -1169,7 +1305,8 @@ function sealMatch(parts, opts = {}) {
|
|
|
1169
1305
|
winnerCode = claimedWinner;
|
|
1170
1306
|
}
|
|
1171
1307
|
}
|
|
1172
|
-
const
|
|
1308
|
+
const timelined = status === "LIVE" || status === "HT" || status === "FT";
|
|
1309
|
+
const events = opts.events === false || !timelined ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e).sort(byClock);
|
|
1173
1310
|
const out = { id, stage };
|
|
1174
1311
|
if (stageLabel2) out.stageLabel = stageLabel2;
|
|
1175
1312
|
if (group) out.group = group;
|
|
@@ -4290,6 +4427,75 @@ function parseMinute(st) {
|
|
|
4290
4427
|
}
|
|
4291
4428
|
return void 0;
|
|
4292
4429
|
}
|
|
4430
|
+
var DETAIL_FLAGS = ["scoringPlay", "yellowCard", "redCard", "penaltyKick", "ownGoal", "shootout"];
|
|
4431
|
+
var DETAIL_MINUTE = /^(\d{1,3})'$/;
|
|
4432
|
+
var DETAIL_MINUTE_ADDED = /^(\d{1,3})'\+(\d{1,2})'$/;
|
|
4433
|
+
function detailKind(d) {
|
|
4434
|
+
const f = {};
|
|
4435
|
+
for (const name of DETAIL_FLAGS) {
|
|
4436
|
+
const v = flag(d[name]);
|
|
4437
|
+
if (v === void 0) return void 0;
|
|
4438
|
+
f[name] = v;
|
|
4439
|
+
}
|
|
4440
|
+
if (f.shootout) return void 0;
|
|
4441
|
+
const { scoringPlay: s, yellowCard: y, redCard: r, penaltyKick: p, ownGoal: o } = f;
|
|
4442
|
+
if (s && !y && !r) {
|
|
4443
|
+
if (!p && !o) return "GOAL";
|
|
4444
|
+
if (o && !p) return "OWN_GOAL";
|
|
4445
|
+
if (p && !o) return "PEN";
|
|
4446
|
+
return void 0;
|
|
4447
|
+
}
|
|
4448
|
+
if (p || o || s) return void 0;
|
|
4449
|
+
if (y && !r) return "YELLOW";
|
|
4450
|
+
if (r && !y) return "RED";
|
|
4451
|
+
return void 0;
|
|
4452
|
+
}
|
|
4453
|
+
function detailMinute(clock) {
|
|
4454
|
+
if (!clock || typeof clock !== "object") return void 0;
|
|
4455
|
+
const dv = clock.displayValue;
|
|
4456
|
+
if (typeof dv !== "string") return void 0;
|
|
4457
|
+
const plain = DETAIL_MINUTE.exec(dv);
|
|
4458
|
+
if (plain) return { minute: Number(plain[1]) };
|
|
4459
|
+
const extra = DETAIL_MINUTE_ADDED.exec(dv);
|
|
4460
|
+
if (!extra) return void 0;
|
|
4461
|
+
return { minute: Number(extra[1]), added: Number(extra[2]) };
|
|
4462
|
+
}
|
|
4463
|
+
function rawTeamId(holder) {
|
|
4464
|
+
if (!holder || typeof holder !== "object") return void 0;
|
|
4465
|
+
const team = holder.team;
|
|
4466
|
+
if (!team || typeof team !== "object") return void 0;
|
|
4467
|
+
return opaqueId(team.id, ESPN_ID);
|
|
4468
|
+
}
|
|
4469
|
+
function detailEvents(details, sides) {
|
|
4470
|
+
const out = [];
|
|
4471
|
+
for (const raw of takeBounded(details, MAX_MATCH_EVENTS)) {
|
|
4472
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
4473
|
+
const d = raw;
|
|
4474
|
+
const type = detailKind(d);
|
|
4475
|
+
if (!type) continue;
|
|
4476
|
+
const when = detailMinute(d.clock);
|
|
4477
|
+
if (!when) continue;
|
|
4478
|
+
const teamId = rawTeamId(d);
|
|
4479
|
+
const side = teamId === void 0 ? void 0 : teamId === sides.homeId ? "home" : teamId === sides.awayId ? "away" : void 0;
|
|
4480
|
+
if (!side) continue;
|
|
4481
|
+
const event = { type, minute: when.minute };
|
|
4482
|
+
if (when.added !== void 0) event.added = when.added;
|
|
4483
|
+
event.side = side;
|
|
4484
|
+
event.teamCode = side === "home" ? sides.homeCode : sides.awayCode;
|
|
4485
|
+
const player = detailPlayer(d.athletesInvolved, type === "OWN_GOAL" ? side === "home" ? sides.awayId : sides.homeId : teamId);
|
|
4486
|
+
if (player) event.player = player;
|
|
4487
|
+
out.push(event);
|
|
4488
|
+
}
|
|
4489
|
+
return out;
|
|
4490
|
+
}
|
|
4491
|
+
function detailPlayer(athletes, expectedTeamId) {
|
|
4492
|
+
if (!Array.isArray(athletes) || athletes.length !== 1 || expectedTeamId === void 0) return void 0;
|
|
4493
|
+
const a = athletes[0];
|
|
4494
|
+
if (rawTeamId(a) !== expectedTeamId) return void 0;
|
|
4495
|
+
const name = a.displayName;
|
|
4496
|
+
if (!isHumanLabel(name)) return void 0;
|
|
4497
|
+
return humanLabel(name) || void 0;
|
|
4498
|
+
}
|
|
4293
4499
|
var MAX_SLUG_UNITS2 = 64;
|
|
4294
4500
|
var SLUG = /^[a-z0-9-]+$/;
|
|
4295
4501
|
var EXACT_STAGE = Object.freeze({
|
|
@@ -4411,7 +4617,14 @@ function parseEspnEvent(raw, ctx = {}) {
|
|
|
4411
4617
|
status,
|
|
4412
4618
|
winnerCode,
|
|
4413
4619
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4414
|
-
|
|
4620
|
+
// The goals and cards: ESPN's `details`, read by the detail grammar; the
|
|
4621
|
+
// seal keeps them only on a status that holds a score.
|
|
4622
|
+
events: detailEvents(comp?.details, {
|
|
4623
|
+
homeId: rawTeamId(homeRaw),
|
|
4624
|
+
awayId: rawTeamId(awayRaw),
|
|
4625
|
+
homeCode: home.code,
|
|
4626
|
+
awayCode: away.code
|
|
4627
|
+
})
|
|
4415
4628
|
},
|
|
4416
4629
|
// The final reseal states the same kind the competitors were built with.
|
|
4417
4630
|
{ teamKind: kind }
|
|
@@ -4728,7 +4941,7 @@ function fetchMeta(result) {
|
|
|
4728
4941
|
}
|
|
4729
4942
|
var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
|
|
4730
4943
|
var DEFAULT_COMPETITION = BUNDLED_SLUG;
|
|
4731
|
-
var USER_AGENT = `claudinho/${"0.
|
|
4944
|
+
var USER_AGENT = `claudinho/${"0.12.0"} (+https://github.com/arturogarrido/claudinho)`;
|
|
4732
4945
|
var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
4733
4946
|
var DEFAULT_COOLDOWN_MS = 5 * 6e4;
|
|
4734
4947
|
var MAX_COOLDOWN_MS = 15 * 6e4;
|
|
@@ -6217,6 +6430,9 @@ function mergeLive(base, live) {
|
|
|
6217
6430
|
function clockOf(adapter) {
|
|
6218
6431
|
return new Date(adapter.now?.() ?? Date.now());
|
|
6219
6432
|
}
|
|
6433
|
+
function requestClock(adapter, given) {
|
|
6434
|
+
return given ?? clockOf(adapter);
|
|
6435
|
+
}
|
|
6220
6436
|
function providerDayOf(adapter, instant) {
|
|
6221
6437
|
if (Number.isNaN(instant.getTime())) return "";
|
|
6222
6438
|
return adapter.bucketDay?.(instant) || instant.toISOString().slice(0, 10);
|
|
@@ -6391,7 +6607,7 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
|
|
|
6391
6607
|
});
|
|
6392
6608
|
let refreshRead = {};
|
|
6393
6609
|
if (candidate) {
|
|
6394
|
-
const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id);
|
|
6610
|
+
const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id, now);
|
|
6395
6611
|
refreshRead = partial ? { partial } : {};
|
|
6396
6612
|
const m = r.degraded ? candidate : r.match ?? candidate;
|
|
6397
6613
|
if (!isFinished(m.status)) return { ...r, match: m, ...bothReads(windowRead, refreshRead) };
|
|
@@ -6600,10 +6816,19 @@ async function getScheduleAhead(adapter, now = /* @__PURE__ */ new Date()) {
|
|
|
6600
6816
|
};
|
|
6601
6817
|
}
|
|
6602
6818
|
async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
|
|
6603
|
-
const { complete: _complete, ...result } = await getLiveRead(adapter, now);
|
|
6819
|
+
const { complete: _complete, finished: _finished, finishedTruncated: _truncated, ...result } = await getLiveRead(adapter, now);
|
|
6604
6820
|
return result;
|
|
6605
6821
|
}
|
|
6606
|
-
|
|
6822
|
+
var MAX_LIVE_FINISHED = 64;
|
|
6823
|
+
function finishedOf(records, seen) {
|
|
6824
|
+
const wanted = new Set(takeBounded(seen, MAX_LIVE_FINISHED));
|
|
6825
|
+
const done = records.filter((m) => m.status === "FT" && m.score !== void 0);
|
|
6826
|
+
const first = done.filter((m) => wanted.has(m.id));
|
|
6827
|
+
const rest = done.filter((m) => !wanted.has(m.id));
|
|
6828
|
+
const all = [...first, ...rest];
|
|
6829
|
+
return { finished: all.slice(0, MAX_LIVE_FINISHED), truncated: all.length > MAX_LIVE_FINISHED };
|
|
6830
|
+
}
|
|
6831
|
+
async function getLiveRead(adapter, now = /* @__PURE__ */ new Date(), seen = []) {
|
|
6607
6832
|
try {
|
|
6608
6833
|
const day = now.toISOString().slice(0, 10);
|
|
6609
6834
|
const fetched = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1), { acrossSeasons: true }) : await adapter.fetchLive();
|
|
@@ -6611,6 +6836,7 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
|
|
|
6611
6836
|
const season = meta?.season;
|
|
6612
6837
|
const matches = fetched.filter((m) => isLive(m.status));
|
|
6613
6838
|
const between = betweenEditionsOf(adapter, meta, fetched, providerDayOf(adapter, now));
|
|
6839
|
+
const done = finishedOf(fetched, seen);
|
|
6614
6840
|
return {
|
|
6615
6841
|
matches,
|
|
6616
6842
|
degraded: false,
|
|
@@ -6622,10 +6848,12 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
|
|
|
6622
6848
|
// boolean below is the refresher's, unchanged: false for that, for an
|
|
6623
6849
|
// adapter that said nothing, and for a failed read alike.
|
|
6624
6850
|
...partialOfRead(meta),
|
|
6625
|
-
complete: meta?.complete === true
|
|
6851
|
+
complete: meta?.complete === true,
|
|
6852
|
+
finished: done.finished,
|
|
6853
|
+
finishedTruncated: done.truncated
|
|
6626
6854
|
};
|
|
6627
6855
|
} catch {
|
|
6628
|
-
return { matches: [], degraded: true, complete: false };
|
|
6856
|
+
return { matches: [], degraded: true, complete: false, finished: [], finishedTruncated: false };
|
|
6629
6857
|
}
|
|
6630
6858
|
}
|
|
6631
6859
|
function norm2(s) {
|
|
@@ -6869,19 +7097,26 @@ function sealMarketSignal(raw, options = {}) {
|
|
|
6869
7097
|
if (sourceMarketId) out.sourceMarketId = sourceMarketId;
|
|
6870
7098
|
out.asOf = canonicalTimestamp(s.asOf) ?? "";
|
|
6871
7099
|
out.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
|
|
7100
|
+
const judgedAt = options.now ?? /* @__PURE__ */ new Date();
|
|
7101
|
+
const fetchedMs = Date.parse(out.fetchedAt);
|
|
7102
|
+
if (out.fetchedAt === "" || !Number.isFinite(fetchedMs)) {
|
|
7103
|
+
return malformed("signal carries no readable fetch time");
|
|
7104
|
+
}
|
|
7105
|
+
if (fetchedMs > judgedAt.getTime()) {
|
|
7106
|
+
return unresolved("signal was fetched after the clock it is judged by");
|
|
7107
|
+
}
|
|
6872
7108
|
out.outcomes = outcomes;
|
|
6873
7109
|
const isAmbiguous = s.ambiguous !== false;
|
|
6874
7110
|
const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
|
|
6875
7111
|
if (favorite) out.favorite = favorite;
|
|
6876
7112
|
if (liquidity !== void 0) out.liquidity = liquidity;
|
|
6877
7113
|
if (volume24h !== void 0) out.volume24h = volume24h;
|
|
6878
|
-
out.stale =
|
|
7114
|
+
out.stale = isStaleSignal(out, { now: judgedAt, maxAgeMs: options.maxAgeMs });
|
|
6879
7115
|
out.ambiguous = isAmbiguous || out.source === "";
|
|
6880
|
-
out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
|
|
6881
7116
|
return valid(out);
|
|
6882
7117
|
}
|
|
6883
7118
|
var DEFAULT_MAX_AGE_MS = 15 * 6e4;
|
|
6884
|
-
function marketRelevant(match, now
|
|
7119
|
+
function marketRelevant(match, now) {
|
|
6885
7120
|
if (isLive(match.status)) return true;
|
|
6886
7121
|
if (isFinished(match.status)) return false;
|
|
6887
7122
|
const k = Date.parse(match.kickoff);
|
|
@@ -6970,12 +7205,13 @@ function buildMarketSignal(input) {
|
|
|
6970
7205
|
const outcomes = normalizeOutcomes(input.outcomes);
|
|
6971
7206
|
const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
|
|
6972
7207
|
const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
|
|
7208
|
+
const clock = input.now ?? /* @__PURE__ */ new Date();
|
|
6973
7209
|
const signal = {
|
|
6974
7210
|
matchId: input.match.id,
|
|
6975
7211
|
source: input.source,
|
|
6976
7212
|
sourceMarketId: input.sourceMarketId,
|
|
6977
7213
|
asOf: input.asOf,
|
|
6978
|
-
fetchedAt: input.fetchedAt ??
|
|
7214
|
+
fetchedAt: input.fetchedAt ?? clock.toISOString(),
|
|
6979
7215
|
outcomes,
|
|
6980
7216
|
favorite,
|
|
6981
7217
|
liquidity: input.liquidity,
|
|
@@ -6983,8 +7219,8 @@ function buildMarketSignal(input) {
|
|
|
6983
7219
|
stale: false,
|
|
6984
7220
|
ambiguous: ambiguous2
|
|
6985
7221
|
};
|
|
6986
|
-
signal.stale = isStaleSignal(signal, { now:
|
|
6987
|
-
const sealed = sealMarketSignal(signal, { now:
|
|
7222
|
+
signal.stale = isStaleSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
|
|
7223
|
+
const sealed = sealMarketSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
|
|
6988
7224
|
if (sealed.kind !== "valid") {
|
|
6989
7225
|
return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
|
|
6990
7226
|
}
|
|
@@ -8079,13 +8315,13 @@ var STATUS_KEY = {
|
|
|
8079
8315
|
function flairOptsOf(opts) {
|
|
8080
8316
|
return { level: opts.flavor, locale: opts.locale, kind: opts.teamKind, pin: opts.pin };
|
|
8081
8317
|
}
|
|
8082
|
-
function matchLine(m, opts
|
|
8318
|
+
function matchLine(m, opts, flair = matchFlair(m, flairOptsOf(opts))) {
|
|
8083
8319
|
const head = `${withFlag(m.home.name, m.home.flag, "home")} ${scoreline(m)} ${withFlag(m.away.name, m.away.flag, "away")}`;
|
|
8084
8320
|
const lang = opts.locale;
|
|
8085
8321
|
const stage = stageLabelI18n(lang, m);
|
|
8086
8322
|
let tail;
|
|
8087
8323
|
if (m.status === "SCHEDULED") {
|
|
8088
|
-
tail = `${formatKickoff(m.kickoff, opts)} (${countdownPhrase(lang, m.kickoff)})`;
|
|
8324
|
+
tail = `${formatKickoff(m.kickoff, opts)} (${countdownPhrase(lang, m.kickoff, opts.now)})`;
|
|
8089
8325
|
} else if (m.status === "LIVE") {
|
|
8090
8326
|
tail = m.minute ? `${t(lang, "status.live")} ${m.minute}'` : t(lang, "status.live");
|
|
8091
8327
|
} else {
|
|
@@ -8104,7 +8340,7 @@ function truncationNote(list) {
|
|
|
8104
8340
|
function headingLine(lang, title) {
|
|
8105
8341
|
return t(lang, "heading", { title });
|
|
8106
8342
|
}
|
|
8107
|
-
function matchRows(matches, empty, opts
|
|
8343
|
+
function matchRows(matches, empty, opts) {
|
|
8108
8344
|
if (matches.length === 0) return empty;
|
|
8109
8345
|
const shown = matches.slice(0, MAX_LIST_MATCHES);
|
|
8110
8346
|
const flairs = matchFlairs(shown, flairOptsOf(opts));
|
|
@@ -8114,7 +8350,7 @@ function listTruncation(matches) {
|
|
|
8114
8350
|
const overflow = matches.length - MAX_LIST_MATCHES;
|
|
8115
8351
|
return overflow > 0 ? `(list truncated \u2014 ${overflow} more not shown)` : void 0;
|
|
8116
8352
|
}
|
|
8117
|
-
function matchList(matches, empty, opts
|
|
8353
|
+
function matchList(matches, empty, opts) {
|
|
8118
8354
|
const rows = matchRows(matches, empty, opts);
|
|
8119
8355
|
const truncated = listTruncation(matches);
|
|
8120
8356
|
return truncated ? `${rows}
|
|
@@ -8326,56 +8562,60 @@ var MARKETS_TOOL_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
|
|
|
8326
8562
|
function memKey(competition, id) {
|
|
8327
8563
|
return `polymarket:${competition}:${id}`;
|
|
8328
8564
|
}
|
|
8329
|
-
async function cachedMarketSignals(args, matches, providerFactory = makeMarketProvider) {
|
|
8565
|
+
async function cachedMarketSignals(args, matches, now, providerFactory = makeMarketProvider) {
|
|
8566
|
+
const clock = { now };
|
|
8330
8567
|
if (args.marketProvider) {
|
|
8331
|
-
const batch = await getMarketSignals(args.marketProvider, matches);
|
|
8568
|
+
const batch = await getMarketSignals(args.marketProvider, matches, clock);
|
|
8332
8569
|
return { signals: resolvedValues(batch), complete: batch.complete };
|
|
8333
8570
|
}
|
|
8334
8571
|
const source = resolveMarketSource();
|
|
8335
8572
|
const competition = competitionOf(args);
|
|
8336
8573
|
if (source !== "polymarket") {
|
|
8337
|
-
const batch = await getMarketSignals(
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
);
|
|
8574
|
+
const batch = await getMarketSignals(providerFactory(source, competition), matches, {
|
|
8575
|
+
...DEFAULT_ON_MARKET_OPTS,
|
|
8576
|
+
...clock
|
|
8577
|
+
});
|
|
8342
8578
|
return { signals: resolvedValues(batch), complete: batch.complete };
|
|
8343
8579
|
}
|
|
8344
|
-
const
|
|
8580
|
+
const memNow = Date.now();
|
|
8345
8581
|
const result = /* @__PURE__ */ new Map();
|
|
8346
8582
|
const miss = [];
|
|
8347
8583
|
for (const m of matches) {
|
|
8348
8584
|
const e = marketMem.get(memKey(competition, m.id));
|
|
8349
8585
|
const ttl = e?.signal ? MEM_POSITIVE_TTL : MEM_NEGATIVE_TTL;
|
|
8350
|
-
const fresh = e &&
|
|
8351
|
-
if (fresh
|
|
8352
|
-
|
|
8586
|
+
const fresh = e && memNow - e.at <= ttl;
|
|
8587
|
+
if (!fresh) {
|
|
8588
|
+
miss.push(m);
|
|
8589
|
+
continue;
|
|
8590
|
+
}
|
|
8591
|
+
if (!e.signal) continue;
|
|
8592
|
+
const resealed = sealMarketSignal(e.signal, { now });
|
|
8593
|
+
if (resealed.kind === "valid" && marketSignalRendersFor(m, resealed.value)) {
|
|
8594
|
+
result.set(m.id, resealed.value);
|
|
8353
8595
|
} else {
|
|
8354
8596
|
miss.push(m);
|
|
8355
8597
|
}
|
|
8356
8598
|
}
|
|
8357
8599
|
let complete = true;
|
|
8358
8600
|
if (miss.length > 0) {
|
|
8359
|
-
const batch = await getMarketSignals(
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
);
|
|
8601
|
+
const batch = await getMarketSignals(providerFactory("polymarket", competition), miss, {
|
|
8602
|
+
...DEFAULT_ON_MARKET_OPTS,
|
|
8603
|
+
...clock
|
|
8604
|
+
});
|
|
8364
8605
|
const fetched = resolvedValues(batch);
|
|
8365
8606
|
complete = batch.complete;
|
|
8366
8607
|
for (const id of cacheableKeys(batch)) {
|
|
8367
|
-
marketMem.set(memKey(competition, id), { at:
|
|
8608
|
+
marketMem.set(memKey(competition, id), { at: memNow, signal: fetched.get(id) ?? null });
|
|
8368
8609
|
}
|
|
8369
8610
|
for (const [id, s] of fetched) result.set(id, s);
|
|
8370
8611
|
}
|
|
8371
8612
|
return { signals: result, complete };
|
|
8372
8613
|
}
|
|
8373
|
-
async function reliableMarketData(args, matches) {
|
|
8614
|
+
async function reliableMarketData(args, matches, now) {
|
|
8374
8615
|
if (!marketsEnabled()) return { data: void 0, complete: true };
|
|
8375
|
-
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
8376
8616
|
const relevant = matches.filter((m) => marketRelevant(m, now));
|
|
8377
8617
|
if (relevant.length === 0) return { data: void 0, complete: true };
|
|
8378
|
-
const result = await cachedMarketSignals(args, relevant);
|
|
8618
|
+
const result = await cachedMarketSignals(args, relevant, now);
|
|
8379
8619
|
const out = {};
|
|
8380
8620
|
for (const m of relevant) {
|
|
8381
8621
|
const s = result.signals.get(m.id);
|
|
@@ -8388,9 +8628,11 @@ async function reliableMarketData(args, matches) {
|
|
|
8388
8628
|
complete: result.complete
|
|
8389
8629
|
};
|
|
8390
8630
|
}
|
|
8391
|
-
function fmtOpts(args, competition) {
|
|
8631
|
+
function fmtOpts(args, competition, now) {
|
|
8392
8632
|
const pin = choiceOf(args).pin;
|
|
8393
8633
|
return {
|
|
8634
|
+
// The tool's clock, read once where the tool reads it: a line's countdown is relative to it.
|
|
8635
|
+
now,
|
|
8394
8636
|
tz: args.tz,
|
|
8395
8637
|
locale: args.lang,
|
|
8396
8638
|
flavor: asFlavorLevel(args.flavor ?? process.env.CLAUDINHO_FLAVOR),
|
|
@@ -8431,17 +8673,18 @@ function toolGetToday(args) {
|
|
|
8431
8673
|
}
|
|
8432
8674
|
async function todayAnswer(args) {
|
|
8433
8675
|
const adapter = resolveAdapter(args);
|
|
8434
|
-
const
|
|
8676
|
+
const now = requestClock(adapter, args.now);
|
|
8677
|
+
const date = args.date ?? localDate(now.toISOString(), args.tz);
|
|
8435
8678
|
const day = await getMatchesForDate(adapter, date, resolveTz(args.tz));
|
|
8436
8679
|
const { matches, degraded, source } = day;
|
|
8437
8680
|
const todays = fixturesByDate(date, matches, args.tz);
|
|
8438
8681
|
const shownToday = boundedRecords(todays);
|
|
8439
|
-
const opts = fmtOpts(args, adapter.competition);
|
|
8682
|
+
const opts = fmtOpts(args, adapter.competition, now);
|
|
8440
8683
|
const empty = verdictNotice(day, args.lang) ?? dateUnreached(day, date, args.lang) ?? dateNoneRead(day, date, args.lang) ?? t(args.lang, "today.none");
|
|
8441
8684
|
const text = `${headingLine(args.lang, t(args.lang, "today.onDate", { date }))}
|
|
8442
8685
|
${matchRows(todays, empty, opts)}`;
|
|
8443
8686
|
const degradedLine = degraded ? `(${t(args.lang, day.skeleton ? "feed.degraded" : "live.degraded")})` : void 0;
|
|
8444
|
-
const market = await reliableMarketData(args, todays);
|
|
8687
|
+
const market = await reliableMarketData(args, todays, now);
|
|
8445
8688
|
const marketLine2 = market.complete ? void 0 : "(Market data unavailable or incomplete \u2014 not all fixtures were checked.)";
|
|
8446
8689
|
const attribution = dayAttribution(day, shownToday.items, args.lang);
|
|
8447
8690
|
return {
|
|
@@ -8479,9 +8722,10 @@ function toolGetLive(args = {}) {
|
|
|
8479
8722
|
}
|
|
8480
8723
|
async function liveAnswer(args) {
|
|
8481
8724
|
const adapter = resolveAdapter(args);
|
|
8482
|
-
const
|
|
8725
|
+
const now = requestClock(adapter, args.now);
|
|
8726
|
+
const live = await getLiveMatches(adapter, now);
|
|
8483
8727
|
const { matches, degraded, source } = live;
|
|
8484
|
-
const opts = fmtOpts(args, adapter.competition);
|
|
8728
|
+
const opts = fmtOpts(args, adapter.competition, now);
|
|
8485
8729
|
const text = degraded ? t(args.lang, "live.degraded") : `${headingLine(args.lang, t(args.lang, "live.title"))}
|
|
8486
8730
|
${matchRows(matches, verdictNotice(live, args.lang) ?? liveNoneRead(live, args.lang) ?? t(args.lang, "live.none"), opts)}`;
|
|
8487
8731
|
const shownLive = boundedRecords(matches);
|
|
@@ -8504,7 +8748,8 @@ function toolGetMatch(args) {
|
|
|
8504
8748
|
}
|
|
8505
8749
|
async function matchAnswer(args) {
|
|
8506
8750
|
const adapter = resolveAdapter(args);
|
|
8507
|
-
const
|
|
8751
|
+
const now = requestClock(adapter, args.now);
|
|
8752
|
+
const found = await getMatchById(adapter, args.id, now);
|
|
8508
8753
|
const { match, degraded, source: liveSource } = found;
|
|
8509
8754
|
if (!match) {
|
|
8510
8755
|
const msg = verdictNotice(found, args.lang) ?? matchWindowSentence(found, args.id, args.lang) ?? matchNoneReadSentence(found, args.id, args.lang) ?? t(args.lang, degraded ? "match.unreachable" : "match.none", { id: args.id });
|
|
@@ -8520,20 +8765,21 @@ async function matchAnswer(args) {
|
|
|
8520
8765
|
}
|
|
8521
8766
|
};
|
|
8522
8767
|
}
|
|
8523
|
-
const opts = fmtOpts(args, adapter.competition);
|
|
8524
|
-
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
8768
|
+
const opts = fmtOpts(args, adapter.competition, now);
|
|
8525
8769
|
let marketSignal;
|
|
8526
8770
|
let marketComplete = true;
|
|
8527
8771
|
if (marketsEnabled() && marketRelevant(match, now)) {
|
|
8528
|
-
const market = await cachedMarketSignals(args, [match]);
|
|
8772
|
+
const market = await cachedMarketSignals(args, [match], now);
|
|
8529
8773
|
marketComplete = market.complete;
|
|
8530
8774
|
const s = market.signals.get(match.id);
|
|
8531
8775
|
if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(match, s)) marketSignal = s;
|
|
8532
8776
|
}
|
|
8533
8777
|
const attribution = dayAttribution(found, [match], args.lang);
|
|
8534
|
-
const
|
|
8535
|
-
|
|
8536
|
-
|
|
8778
|
+
const text = [
|
|
8779
|
+
matchLine(match, opts),
|
|
8780
|
+
...marketSignal ? marketBlock(marketSignal, match) : [],
|
|
8781
|
+
...timelineRows(match, { lang: args.lang, flags: true })
|
|
8782
|
+
].join("\n");
|
|
8537
8783
|
const degradedLine = degraded ? `(${t(args.lang, found.earlierRecord ? "feed.earlierRecord" : "feed.degraded")})` : void 0;
|
|
8538
8784
|
const marketLine2 = marketComplete ? void 0 : "(Market data unavailable or incomplete \u2014 this match was not checked.)";
|
|
8539
8785
|
return {
|
|
@@ -8696,7 +8942,7 @@ async function nextAnswer(args) {
|
|
|
8696
8942
|
if (!query) throw new Error(NO_TEAM);
|
|
8697
8943
|
const asked = pin ? { code: query } : bundleApplies(adapter.competition) ? nationArg(query) : { code: query };
|
|
8698
8944
|
const code = "code" in asked ? asked.code : query;
|
|
8699
|
-
const now = args.now
|
|
8945
|
+
const now = requestClock(adapter, args.now);
|
|
8700
8946
|
const next = pin ? await nextFixtureForPin(adapter, pin, now) : "code" in asked ? await getNextFixtureForTeam(adapter, code, now) : asked.answer;
|
|
8701
8947
|
const { fixture, degraded, source } = next;
|
|
8702
8948
|
const label = nextTeamLabel(next, code);
|
|
@@ -8716,7 +8962,7 @@ ${msg}`, next, args.lang), void 0, args.lang),
|
|
|
8716
8962
|
data: { ...about, fixture: null, degraded, source: source ?? null, ...verdictExtras(next) }
|
|
8717
8963
|
};
|
|
8718
8964
|
}
|
|
8719
|
-
const opts = fmtOpts(args, adapter.competition);
|
|
8965
|
+
const opts = fmtOpts(args, adapter.competition, now);
|
|
8720
8966
|
return {
|
|
8721
8967
|
// `source` in data mirrors the text's "Live data: …" attribution (parity
|
|
8722
8968
|
// with CLI `next --json`); null for a static group fixture (no live source).
|
|
@@ -8760,7 +9006,6 @@ function toolGetMarketSignal(args) {
|
|
|
8760
9006
|
}
|
|
8761
9007
|
async function marketAnswer(args) {
|
|
8762
9008
|
const provider = resolveMarketProvider(args);
|
|
8763
|
-
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
8764
9009
|
if (args.matchId) {
|
|
8765
9010
|
const scope = marketScopeVerdict(competitionOf(args), 0);
|
|
8766
9011
|
const outOfScope = verdictNotice(scope, args.lang);
|
|
@@ -8776,13 +9021,15 @@ async function marketAnswer(args) {
|
|
|
8776
9021
|
}
|
|
8777
9022
|
};
|
|
8778
9023
|
}
|
|
8779
|
-
const
|
|
9024
|
+
const adapter2 = resolveAdapter(args);
|
|
9025
|
+
const now2 = requestClock(adapter2, args.now);
|
|
9026
|
+
const found = await getMatchById(adapter2, args.matchId, now2);
|
|
8780
9027
|
const { match } = found;
|
|
8781
|
-
const relevant = match ? marketRelevant(match,
|
|
8782
|
-
const batch2 = match && relevant ? await getMarketSignals(provider, [match], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
|
|
9028
|
+
const relevant = match ? marketRelevant(match, now2) : false;
|
|
9029
|
+
const batch2 = match && relevant ? await getMarketSignals(provider, [match], { ...MARKETS_TOOL_OPTS, now: now2 }) : { results: /* @__PURE__ */ new Map(), complete: true };
|
|
8783
9030
|
const sig = match ? resolvedValues(batch2).get(match.id) : void 0;
|
|
8784
9031
|
const shown2 = batch2.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
|
|
8785
|
-
const text2 = !match ? verdictNotice(found, args.lang) ?? `No match found with id ${args.matchId}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(match, args)} \u2014 this match could not be checked.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args,
|
|
9032
|
+
const text2 = !match ? verdictNotice(found, args.lang) ?? `No match found with id ${args.matchId}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(match, args)} \u2014 this match could not be checked.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now2);
|
|
8786
9033
|
return {
|
|
8787
9034
|
// The fixture read was not whole: said first, apart from `complete`
|
|
8788
9035
|
// (the market requests' own completeness).
|
|
@@ -8798,13 +9045,15 @@ async function marketAnswer(args) {
|
|
|
8798
9045
|
}
|
|
8799
9046
|
if (args.team) {
|
|
8800
9047
|
const code = args.team.toUpperCase();
|
|
8801
|
-
const
|
|
9048
|
+
const adapter2 = resolveAdapter(args);
|
|
9049
|
+
const now2 = requestClock(adapter2, args.now);
|
|
9050
|
+
const picked = await marketFixtureForTeam(adapter2, code, now2);
|
|
8802
9051
|
const { match: fixture, degraded } = picked;
|
|
8803
|
-
const relevant = fixture ? marketRelevant(fixture,
|
|
8804
|
-
const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
|
|
9052
|
+
const relevant = fixture ? marketRelevant(fixture, now2) : false;
|
|
9053
|
+
const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], { ...MARKETS_TOOL_OPTS, now: now2 }) : { results: /* @__PURE__ */ new Map(), complete: true };
|
|
8805
9054
|
const sig = fixture ? resolvedValues(batch2).get(fixture.id) : void 0;
|
|
8806
9055
|
const shown2 = batch2.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
|
|
8807
|
-
const text2 = !fixture ? verdictNotice(picked, args.lang) ?? (degraded ? `Live feed unavailable \u2014 can't resolve ${code}'s next fixture right now.` : `No upcoming fixture found for ${code}.`) : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(fixture, args)} \u2014 this match could not be checked.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args,
|
|
9056
|
+
const text2 = !fixture ? verdictNotice(picked, args.lang) ?? (degraded ? `Live feed unavailable \u2014 can't resolve ${code}'s next fixture right now.` : `No upcoming fixture found for ${code}.`) : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(fixture, args)} \u2014 this match could not be checked.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now2);
|
|
8808
9057
|
return {
|
|
8809
9058
|
// Either read the pick was made from was not whole: said first, apart
|
|
8810
9059
|
// from `complete` (the market requests' own completeness).
|
|
@@ -8820,11 +9069,13 @@ async function marketAnswer(args) {
|
|
|
8820
9069
|
}
|
|
8821
9070
|
};
|
|
8822
9071
|
}
|
|
9072
|
+
const adapter = resolveAdapter(args);
|
|
9073
|
+
const now = requestClock(adapter, args.now);
|
|
8823
9074
|
const date = args.date ?? localDate(now.toISOString(), args.tz);
|
|
8824
|
-
const day = await getMatchesForDate(
|
|
9075
|
+
const day = await getMatchesForDate(adapter, date, resolveTz(args.tz));
|
|
8825
9076
|
const fixtureRead = marketsCoverCompetition(competitionOf(args)) ? day : {};
|
|
8826
9077
|
const todays = fixturesByDate(date, day.matches, args.tz).filter((m) => marketRelevant(m, now));
|
|
8827
|
-
const batch = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
|
|
9078
|
+
const batch = await getMarketSignals(provider, todays, { ...MARKETS_TOOL_OPTS, now });
|
|
8828
9079
|
const signals = resolvedValues(batch);
|
|
8829
9080
|
const all = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
|
|
8830
9081
|
(r) => !!r.signal && marketDisplayable(r.match, r.signal)
|
|
@@ -8863,12 +9114,11 @@ ${shown.items.map(({ match, signal }) => marketText(match, signal, args)).join("
|
|
|
8863
9114
|
}
|
|
8864
9115
|
};
|
|
8865
9116
|
}
|
|
8866
|
-
async function reliableSignalMap(args, matches) {
|
|
9117
|
+
async function reliableSignalMap(args, matches, now) {
|
|
8867
9118
|
if (!marketsEnabled()) return { signals: /* @__PURE__ */ new Map(), complete: true };
|
|
8868
|
-
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
8869
9119
|
const relevant = matches.filter((m) => marketRelevant(m, now));
|
|
8870
9120
|
if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
|
|
8871
|
-
const result = await cachedMarketSignals(args, relevant);
|
|
9121
|
+
const result = await cachedMarketSignals(args, relevant, now);
|
|
8872
9122
|
const out = /* @__PURE__ */ new Map();
|
|
8873
9123
|
for (const m of relevant) {
|
|
8874
9124
|
const s = result.signals.get(m.id);
|
|
@@ -8938,11 +9188,12 @@ function shareKindAsked(args) {
|
|
|
8938
9188
|
}
|
|
8939
9189
|
async function shareAnswer(args) {
|
|
8940
9190
|
const options = shareOptions(args);
|
|
8941
|
-
const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve({ signals: /* @__PURE__ */ new Map(), complete: true }) : reliableSignalMap(args, ms);
|
|
9191
|
+
const signalsFor = (ms, now2) => args.includeMarkets === false ? Promise.resolve({ signals: /* @__PURE__ */ new Map(), complete: true }) : reliableSignalMap(args, ms, now2);
|
|
8942
9192
|
const competition = selectedOf(args).slug;
|
|
8943
9193
|
const where = { tz: args.tz, locale: args.lang, competition };
|
|
8944
9194
|
if (args.live) {
|
|
8945
|
-
const
|
|
9195
|
+
const adapter2 = resolveAdapter(args);
|
|
9196
|
+
const live = await getLiveMatches(adapter2, requestClock(adapter2, args.now));
|
|
8946
9197
|
const shownLive = boundedRecords(live.matches);
|
|
8947
9198
|
return shareResult(
|
|
8948
9199
|
liveShareCard(live, where, {
|
|
@@ -9012,24 +9263,29 @@ async function shareAnswer(args) {
|
|
|
9012
9263
|
};
|
|
9013
9264
|
}
|
|
9014
9265
|
if (args.matchId) {
|
|
9015
|
-
const
|
|
9016
|
-
const
|
|
9266
|
+
const adapter2 = resolveAdapter(args);
|
|
9267
|
+
const now2 = requestClock(adapter2, args.now);
|
|
9268
|
+
const found = await getMatchById(adapter2, args.matchId, now2);
|
|
9269
|
+
const market2 = await signalsFor(found.match ? [found.match] : [], now2);
|
|
9017
9270
|
return shareResult(matchShareCard(found, args.matchId, market2, where), options);
|
|
9018
9271
|
}
|
|
9019
9272
|
if (args.team) {
|
|
9020
|
-
const
|
|
9273
|
+
const adapter2 = resolveAdapter(args);
|
|
9021
9274
|
const query = humanLabel(args.team, 40);
|
|
9022
|
-
const asked = bundleApplies(
|
|
9275
|
+
const asked = bundleApplies(adapter2.competition) ? nationArg(query) : { code: query };
|
|
9023
9276
|
const code = "code" in asked ? asked.code : query;
|
|
9024
|
-
const
|
|
9025
|
-
const
|
|
9277
|
+
const now2 = requestClock(adapter2, args.now);
|
|
9278
|
+
const next = "code" in asked ? await getNextFixtureForTeam(adapter2, code, now2) : asked.answer;
|
|
9279
|
+
const market2 = await signalsFor(next.fixture ? [next.fixture] : [], now2);
|
|
9026
9280
|
return shareResult(nextShareCard(next, code, market2, where), options);
|
|
9027
9281
|
}
|
|
9028
|
-
const
|
|
9029
|
-
const
|
|
9282
|
+
const adapter = resolveAdapter(args);
|
|
9283
|
+
const now = requestClock(adapter, args.now);
|
|
9284
|
+
const date = args.date ?? localDate(now.toISOString(), args.tz);
|
|
9285
|
+
const day = await getMatchesForDate(adapter, date, resolveTz(args.tz));
|
|
9030
9286
|
const todays = fixturesByDate(date, day.matches, args.tz);
|
|
9031
9287
|
const shownToday = boundedRecords(todays);
|
|
9032
|
-
const market = await signalsFor(shownToday.items);
|
|
9288
|
+
const market = await signalsFor(shownToday.items, now);
|
|
9033
9289
|
return shareResult(
|
|
9034
9290
|
dateShareCard(
|
|
9035
9291
|
{
|
|
@@ -9055,7 +9311,7 @@ async function shareAnswer(args) {
|
|
|
9055
9311
|
|
|
9056
9312
|
// src/server.ts
|
|
9057
9313
|
var SERVER_NAME = "claudinho";
|
|
9058
|
-
var SERVER_VERSION = "0.
|
|
9314
|
+
var SERVER_VERSION = "0.12.0";
|
|
9059
9315
|
var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
|
|
9060
9316
|
Voice: when relaying scores, narrate with lively, regionally-appropriate football-commentary energy in the user's language. Each match line may end with a short exclamation ("\u2014 \xA1GOOOOL!"): use it as a tone cue. Keep every fact exact; never invent details and never impersonate or name a real commentator.`;
|
|
9061
9317
|
var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and standings for one football competition per request, chosen by the tool call's competition argument (an alias such as premier-league, or an ESPN slug such as eng.1), else the server's CLAUDINHO_COMPETITION, else the user's saved choice (set with claudinho follow <alias>). With none of the three, every competition-answering tool answers noCompetition with a sentence saying what to do: pass competition, or ask the user to run claudinho follow <alias>. list_competitions lists the supported competitions, their aliases and what each offers, offline. Every tool's text but get_team's, get_share_snippet's and list_competitions' starts with the competition it is for, and its structured data carries it as competition (get_share_snippet's card names it in its title; list_competitions says it last, as Current, and in data.current; get_team is the World Cup's roster). An unknown competition is a tool error that lists the aliases.
|
|
@@ -9082,7 +9338,7 @@ var competitionArg = z.string().describe(
|
|
|
9082
9338
|
"The competition: an alias such as premier-league, or an ESPN slug such as eng.1 (list_competitions lists the aliases). Without it: the server's CLAUDINHO_COMPETITION, else the user's saved choice (claudinho follow); with none, the answer is noCompetition"
|
|
9083
9339
|
);
|
|
9084
9340
|
var langArg = z.string().optional().describe(
|
|
9085
|
-
"Locale for dates, list titles, match status and stage words, the empty and outage sentences, provider attribution, and commentary: en, es, pt, fr (the market copy, a few notes and the disclaimer stay English; other locales fall back to en)"
|
|
9341
|
+
"Locale for dates, list titles, match status and stage words, a match timeline's kind words (Goal, Yellow card\u2026), the empty and outage sentences, provider attribution, and commentary: en, es, pt, fr (the market copy, a few notes and the disclaimer stay English; other locales fall back to en)"
|
|
9086
9342
|
);
|
|
9087
9343
|
var commonArgs = {
|
|
9088
9344
|
competition: competitionArg.optional(),
|
|
@@ -9092,6 +9348,14 @@ var commonArgs = {
|
|
|
9092
9348
|
};
|
|
9093
9349
|
var teamRef = z.object({ code: z.string(), name: z.string(), flag: z.string(), id: z.string() }).partial().passthrough();
|
|
9094
9350
|
var scorePair = z.object({ home: z.number(), away: z.number() }).partial().passthrough();
|
|
9351
|
+
var matchEventOut = z.object({
|
|
9352
|
+
type: z.string(),
|
|
9353
|
+
minute: z.number(),
|
|
9354
|
+
added: z.number(),
|
|
9355
|
+
side: z.string(),
|
|
9356
|
+
teamCode: z.string(),
|
|
9357
|
+
player: z.string()
|
|
9358
|
+
}).partial().passthrough();
|
|
9095
9359
|
var matchOut = z.object({
|
|
9096
9360
|
id: z.string(),
|
|
9097
9361
|
stage: z.string().optional(),
|
|
@@ -9107,6 +9371,9 @@ var matchOut = z.object({
|
|
|
9107
9371
|
shootout: scorePair.optional(),
|
|
9108
9372
|
status: z.string().optional(),
|
|
9109
9373
|
minute: z.number().nullable().optional(),
|
|
9374
|
+
// The goals and cards that could be read, in time order; absent when none
|
|
9375
|
+
// (a match with no score has none).
|
|
9376
|
+
events: z.array(matchEventOut).optional(),
|
|
9110
9377
|
winnerCode: z.string().optional()
|
|
9111
9378
|
}).passthrough();
|
|
9112
9379
|
var anyObj = z.object({}).passthrough();
|
|
@@ -9525,7 +9792,7 @@ function buildServer() {
|
|
|
9525
9792
|
"get_match",
|
|
9526
9793
|
{
|
|
9527
9794
|
title: "Match detail",
|
|
9528
|
-
description: `One match by its id, with live score/minute overlaid when it's in play. Off the World Cup the id is looked for from yesterday to 14 days ahead: window names the provider days searched when it was not there (not found in that span, not "no such match"), and degraded with a match means its state could not be refreshed. Optional prediction-market enrichment carries marketComplete; false means the read was incomplete, not that no signal exists. Get the id from get_today or get_live; to find a team's match without an id, use get_next_fixture. tz/lang/flavor affect formatting.`,
|
|
9795
|
+
description: `One match by its id, with live score/minute overlaid when it's in play, and, for a match in play or finished, the provider's goals and cards it could read (minute, kind, side, the player when named; match.events in data), never a claim that the list is every goal. Off the World Cup the id is looked for from yesterday to 14 days ahead: window names the provider days searched when it was not there (not found in that span, not "no such match"), and degraded with a match means its state could not be refreshed. Optional prediction-market enrichment carries marketComplete; false means the read was incomplete, not that no signal exists. Get the id from get_today or get_live; to find a team's match without an id, use get_next_fixture. tz/lang/flavor affect formatting.`,
|
|
9529
9796
|
inputSchema: { id: z.string().describe("Match id"), ...commonArgs },
|
|
9530
9797
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
9531
9798
|
outputSchema: matchDetailOut
|
|
@@ -9674,8 +9941,9 @@ function buildServer() {
|
|
|
9674
9941
|
},
|
|
9675
9942
|
async (uri, variables) => {
|
|
9676
9943
|
const date = String(variables.date ?? "");
|
|
9944
|
+
const now = /* @__PURE__ */ new Date();
|
|
9677
9945
|
const text = `${competitionLabel(BUNDLE_COMPETITION)}
|
|
9678
|
-
${matchList(fixturesByDate(date, void 0, "UTC"), `No matches on ${date}
|
|
9946
|
+
${matchList(fixturesByDate(date, void 0, "UTC"), `No matches on ${date}.`, { now })}`;
|
|
9679
9947
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text }] };
|
|
9680
9948
|
}
|
|
9681
9949
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudinho/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"mcpName": "io.github.arturogarrido/claudinho",
|
|
5
5
|
"description": "An MCP server with live football scores, fixtures, standings and paste-ready match cards for the competition you follow: the World Cup, the Premier League, LALIGA, the Champions League and 12 more. Works with Claude Code, Cursor, Codex, Claude Desktop, Windsurf, Zed and any MCP client. No API key. Independent fan project. Not affiliated with FIFA, any confederation, league or club, or Anthropic.",
|
|
6
6
|
"type": "module",
|
|
@@ -64,16 +64,16 @@
|
|
|
64
64
|
"club-world-cup"
|
|
65
65
|
],
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
68
68
|
"zod": "^4.6.5"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@types/node": "^22.20.
|
|
71
|
+
"@types/node": "^22.20.5",
|
|
72
72
|
"@vitest/coverage-v8": "^5.0.3",
|
|
73
73
|
"tsup": "^8.0.0",
|
|
74
74
|
"typescript": "^5.7.0",
|
|
75
75
|
"vitest": "^5.0.3",
|
|
76
|
-
"@claudinho/core": "0.
|
|
76
|
+
"@claudinho/core": "0.12.0"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"build": "tsup",
|