@claudinho/mcp 0.11.2 → 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.
Files changed (3) hide show
  1. package/README.md +117 -112
  2. package/dist/index.js +297 -46
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,149 +1,154 @@
1
1
  # @claudinho/mcp ⚽
2
2
 
3
- **An MCP server with live football scores, fixtures, standings and paste-ready match cards for
4
- the competition you follow: the World Cup, the Premier League, LALIGA, the Champions League and
5
- 12 more.** Ask your agent in Claude Code, Cursor, Codex, Claude Desktop, Windsurf, Zed, VS Code,
6
- or any MCP client (stdio). No API key, no signup. The World Cup's 104 fixtures ship bundled; the
7
- other competitions' schedules are read from the feed, and live scores (and the optional,
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="An AI coding agent mid-task during the 2026 World Cup: asked 'did Germany get through?', it calls the Claudinho MCP server and answers with the result: 🇩🇪 Germany 1(3)–1(4) Paraguay 🇵🇾, FT (Paraguay won the shootout)" width="760">
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
- **Cursor**: add to `~/.cursor/mcp.json` (global) or a project `.cursor/mcp.json`:
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
- **Codex CLI**
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
- codex mcp add claudinho -- npx -y @claudinho/mcp
48
+ npx @claudinho/cli follow champions-league --team Arsenal
34
49
  ```
35
50
 
36
- **Claude Desktop / Windsurf / Zed / VS Code**: standard stdio config (same JSON as Cursor):
37
- Claude Desktop: Settings → Developer → Edit Config, then restart. Codex config file:
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
- > "What matches are on today?" · "Show me the live scores" · "What's Arsenal's next game?"
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), grouped in the caller's `tz`, live scores overlaid |
50
- | `get_live` | matches in play right now |
51
- | `get_match` | a single match by id; in a club competition, looked for from yesterday to 14 days ahead (`window` names the days searched when it is not there) |
52
- | `get_standings` | live cumulative standings: every table, or one by its key (a group letter, or `A1`, `A-B`, `LEAGUE`) |
53
- | `get_bracket` | knockout bracket from the Round of 32 through the final, with an optional `stage` filter (`R32`, `R16`, `QF`, `SF`, `3P`, `F`); the Premier League and LALIGA, league seasons with no knockout tie of their own, answer `inapplicable`; the other competitions off the World Cup answer `unsupported` (not offered yet) |
54
- | `get_next_fixture` | a team's next match, by name or code. A nation (`Mexico`, `MEX`): live-resolves a confirmed knockout tie from the feed, group fixtures offline, fails back to the bundled schedule if the feed is down; a club (`Arsenal`, `ARS`): its next match within 14 days, `candidates` when several teams match, `horizon` when it has none in that span. 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 |
55
- | `get_market_signal` | read-only prediction-market signal for a match, a team's current-or-next fixture (in-play preferred while live), or a date; informational only |
56
- | `get_share_snippet` | a copy-pasteable plain-text card for a match, a team's next fixture, a group's standings table (`group`), the knockout bracket (`bracket: true`, optional `knockoutStage`), a date, or live; hand the returned snippet to the user as-is |
57
- | `get_team` | the World Cup roster: resolve a nation name or code to its FIFA 3-letter code, flag, and group; fuzzy (`Mexico`, `mex`, `DR Congo`, `Türkiye`); handy for the code `get_market_signal` needs. It knows no clubs. Offline (no network) |
58
- | `list_competitions` | the supported competitions: each one's alias (what `competition` takes), name, teams (nations or clubs), kind, and what it offers (scores, next, standings, bracket, markets: `offered`, `not-offered-yet`, `not-applicable`), plus `current`, the competition the request is for. Offline (no network) |
59
-
60
- Most tools are **read-only** (`readOnlyHint`) and accept optional `tz`, `lang`
61
- (`en`/`es`/`pt`/`fr`), `flavor` (`off`/`subtle`/`full`), and `competition`: an alias
62
- such as `premier-league` (from `list_competitions`) or an ESPN slug such as `eng.1`
63
- (without it: the server's `CLAUDINHO_COMPETITION`, else the user's saved choice, the one
64
- `claudinho follow <alias>` saves; an unknown value is a tool error listing the aliases).
65
- With none of the three, every competition-answering tool answers, before it reads
66
- anything, `noCompetition: true` and `competition: null` beside its empty shape, and a
67
- sentence saying what to do (pass `competition`, or the user runs `claudinho follow
68
- <alias>`); `list_competitions` answers `current: null`, and `get_team` answers as ever. Every competition-answering tool's text
69
- starts with the competition it is for (`Premier League · from the request`; a share
70
- card says it in its title instead) and its structured content carries it as
71
- `competition`. `get_team` (the World Cup's roster, whatever the competition) and
72
- `list_competitions` are read-only **and** offline. Every response carries
73
- human-readable text **and** structured content, validated against each tool's
74
- declared `outputSchema`. When the provider's knockout answer was not whole,
75
- `get_next_fixture`, `get_bracket` and the next/bracket share cards say so before
76
- what was read and carry `partial` (`omitted`: the count of provider records left
77
- out, when known); no fixture is then not "eliminated". `get_today`, `get_live`,
78
- `get_match`, `get_market_signal` and their share cards carry `partial` for their own
79
- reads too: an empty answer then means nothing was read, not that nothing exists, and a
80
- World Cup day counts the fixtures shown from the bundled schedule without their live
81
- state. In a club competition whose
82
- edition has ended (and the next has not started), `get_today`, `get_live`,
83
- `get_next_fixture` and `get_match` say so and carry `betweenEditions`. A club has no
84
- `flag` (a nation's is generated from its name), and nothing is printed in its place; a
85
- match's `stage` is the written one (`REGULAR` for a league's season, `LEAGUE` for a cup's
86
- league phase, `PO` for play-offs), or `OTHER` with the provider's own words in `stageLabel`.
87
-
88
- Resources: `standings://{group}` (its text starts with the competition it is for, like a tool's), `fixtures://{date}` (the bundled World Cup schedule, named first, whatever the competition). Prompts: `tournament_today`,
89
- and `my_team` (the competition you follow: give it a club's or a nation's code or name, or
90
- none for your team (the server's `CLAUDINHO_TEAM`, else the one pinned with `claudinho follow --team`);
91
- it asks for the next fixture with its date and state and the team's standing, and on the World Cup
92
- for the prediction-market read of that fixture).
93
-
94
- ## Market signals
95
-
96
- Market signals are pre-match and in-play reads: finished matches never show one.
97
- `get_today` / `get_match` include a short market line when a reliable market exists
98
- (slugs are auto-derived per fixture; matching fails closed). Their structured output
99
- includes `marketComplete`; the dedicated market tool uses `complete`. A false verdict
100
- means the optional read was unavailable or incomplete and is never described as "no signal."
101
- Share cards carry the same verdict and warning. **Read-only and
102
- informational only, not betting advice:** market-implied percentages with Polymarket
103
- attribution, never links or trade calls. Disable with `CLAUDINHO_MARKETS=off`; set
104
- `CLAUDINHO_MARKETS_SOURCE=fake` in the server `env` for a network-free, clearly
105
- labeled synthetic preview.
106
-
107
- ## Commentary flair
108
-
109
- Match lines in the text end with a short, localized exclamation for the match's
110
- moment (`— ¡GOOOOL!`): a few hundred lines in four languages, no real person named
111
- or impersonated, none repeated within one list while the bank lasts, never in the
112
- structured JSON. A team's own rally cry takes its place when one side has one
113
- (Mexico's `¿Y si sí?`, Pumas' `¡Goya!`, Arsenal's `COYG!`): when both do, the team
114
- saved with `claudinho follow --team` for that competition, else the home side's
115
- (the server's `CLAUDINHO_TEAM` is the team-taking tools' default query, not the
116
- cry's tiebreak). A row with a cry takes no line from the bank, a postponed or
117
- cancelled match prints neither, and the `fixtures://` resource prints a phrase,
118
- never a cry (it states no team kind). Control with `CLAUDINHO_FLAVOR`
119
- (`off`|`subtle`|`full`, default `full`; `subtle` is goals, full time, draws and
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 bundled in the package; the other competitions'
125
- schedules are read from the feed, and live state always hits the network: live scores
126
- from **ESPN's** public scoreboard (swappable provider, attributed in output), market
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 Policy
140
+ ## Privacy
131
141
 
132
- No personal data collected: no accounts, no telemetry, no analytics, no tracking, and no
133
- Claudinho server. To show live scores, Claudinho makes read-only requests to public services
134
- (ESPN; Polymarket for informational-only market signals) with no account or personal data
135
- attached, though those services still receive standard request metadata (such as your IP
136
- address) like any HTTP call. The MCP server keeps its cache **in memory only** and writes
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 & issues](https://github.com/arturogarrido/claudinho)
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 :',
@@ -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 void 0;
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 void 0;
1243
+ if (!type) return null;
1121
1244
  const minute = count(e.minute, MAX_MINUTE);
1122
- if (minute === void 0) return void 0;
1123
- const out = { type, minute, teamCode: humanLabel(e.teamCode, TEAM_CODE_COLUMNS) };
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 events = opts.events === false ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e);
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
- events: ev.events
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.11.2"} (+https://github.com/arturogarrido/claudinho)`;
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;
@@ -6603,10 +6816,19 @@ async function getScheduleAhead(adapter, now = /* @__PURE__ */ new Date()) {
6603
6816
  };
6604
6817
  }
6605
6818
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
6606
- const { complete: _complete, ...result } = await getLiveRead(adapter, now);
6819
+ const { complete: _complete, finished: _finished, finishedTruncated: _truncated, ...result } = await getLiveRead(adapter, now);
6607
6820
  return result;
6608
6821
  }
6609
- async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
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 = []) {
6610
6832
  try {
6611
6833
  const day = now.toISOString().slice(0, 10);
6612
6834
  const fetched = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1), { acrossSeasons: true }) : await adapter.fetchLive();
@@ -6614,6 +6836,7 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
6614
6836
  const season = meta?.season;
6615
6837
  const matches = fetched.filter((m) => isLive(m.status));
6616
6838
  const between = betweenEditionsOf(adapter, meta, fetched, providerDayOf(adapter, now));
6839
+ const done = finishedOf(fetched, seen);
6617
6840
  return {
6618
6841
  matches,
6619
6842
  degraded: false,
@@ -6625,10 +6848,12 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
6625
6848
  // boolean below is the refresher's, unchanged: false for that, for an
6626
6849
  // adapter that said nothing, and for a failed read alike.
6627
6850
  ...partialOfRead(meta),
6628
- complete: meta?.complete === true
6851
+ complete: meta?.complete === true,
6852
+ finished: done.finished,
6853
+ finishedTruncated: done.truncated
6629
6854
  };
6630
6855
  } catch {
6631
- return { matches: [], degraded: true, complete: false };
6856
+ return { matches: [], degraded: true, complete: false, finished: [], finishedTruncated: false };
6632
6857
  }
6633
6858
  }
6634
6859
  function norm2(s) {
@@ -6872,19 +7097,26 @@ function sealMarketSignal(raw, options = {}) {
6872
7097
  if (sourceMarketId) out.sourceMarketId = sourceMarketId;
6873
7098
  out.asOf = canonicalTimestamp(s.asOf) ?? "";
6874
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
+ }
6875
7108
  out.outcomes = outcomes;
6876
7109
  const isAmbiguous = s.ambiguous !== false;
6877
7110
  const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
6878
7111
  if (favorite) out.favorite = favorite;
6879
7112
  if (liquidity !== void 0) out.liquidity = liquidity;
6880
7113
  if (volume24h !== void 0) out.volume24h = volume24h;
6881
- out.stale = s.stale !== false;
7114
+ out.stale = isStaleSignal(out, { now: judgedAt, maxAgeMs: options.maxAgeMs });
6882
7115
  out.ambiguous = isAmbiguous || out.source === "";
6883
- out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
6884
7116
  return valid(out);
6885
7117
  }
6886
7118
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
6887
- function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
7119
+ function marketRelevant(match, now) {
6888
7120
  if (isLive(match.status)) return true;
6889
7121
  if (isFinished(match.status)) return false;
6890
7122
  const k = Date.parse(match.kickoff);
@@ -6973,12 +7205,13 @@ function buildMarketSignal(input) {
6973
7205
  const outcomes = normalizeOutcomes(input.outcomes);
6974
7206
  const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
6975
7207
  const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
7208
+ const clock = input.now ?? /* @__PURE__ */ new Date();
6976
7209
  const signal = {
6977
7210
  matchId: input.match.id,
6978
7211
  source: input.source,
6979
7212
  sourceMarketId: input.sourceMarketId,
6980
7213
  asOf: input.asOf,
6981
- fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7214
+ fetchedAt: input.fetchedAt ?? clock.toISOString(),
6982
7215
  outcomes,
6983
7216
  favorite,
6984
7217
  liquidity: input.liquidity,
@@ -6986,8 +7219,8 @@ function buildMarketSignal(input) {
6986
7219
  stale: false,
6987
7220
  ambiguous: ambiguous2
6988
7221
  };
6989
- signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
6990
- const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
7222
+ signal.stale = isStaleSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
7223
+ const sealed = sealMarketSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
6991
7224
  if (sealed.kind !== "valid") {
6992
7225
  return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
6993
7226
  }
@@ -8329,45 +8562,50 @@ var MARKETS_TOOL_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
8329
8562
  function memKey(competition, id) {
8330
8563
  return `polymarket:${competition}:${id}`;
8331
8564
  }
8332
- async function cachedMarketSignals(args, matches, providerFactory = makeMarketProvider) {
8565
+ async function cachedMarketSignals(args, matches, now, providerFactory = makeMarketProvider) {
8566
+ const clock = { now };
8333
8567
  if (args.marketProvider) {
8334
- const batch = await getMarketSignals(args.marketProvider, matches);
8568
+ const batch = await getMarketSignals(args.marketProvider, matches, clock);
8335
8569
  return { signals: resolvedValues(batch), complete: batch.complete };
8336
8570
  }
8337
8571
  const source = resolveMarketSource();
8338
8572
  const competition = competitionOf(args);
8339
8573
  if (source !== "polymarket") {
8340
- const batch = await getMarketSignals(
8341
- providerFactory(source, competition),
8342
- matches,
8343
- DEFAULT_ON_MARKET_OPTS
8344
- );
8574
+ const batch = await getMarketSignals(providerFactory(source, competition), matches, {
8575
+ ...DEFAULT_ON_MARKET_OPTS,
8576
+ ...clock
8577
+ });
8345
8578
  return { signals: resolvedValues(batch), complete: batch.complete };
8346
8579
  }
8347
- const now = Date.now();
8580
+ const memNow = Date.now();
8348
8581
  const result = /* @__PURE__ */ new Map();
8349
8582
  const miss = [];
8350
8583
  for (const m of matches) {
8351
8584
  const e = marketMem.get(memKey(competition, m.id));
8352
8585
  const ttl = e?.signal ? MEM_POSITIVE_TTL : MEM_NEGATIVE_TTL;
8353
- const fresh = e && now - e.at <= ttl;
8354
- if (fresh && (!e.signal || marketSignalRendersFor(m, e.signal))) {
8355
- if (e.signal) result.set(m.id, e.signal);
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);
8356
8595
  } else {
8357
8596
  miss.push(m);
8358
8597
  }
8359
8598
  }
8360
8599
  let complete = true;
8361
8600
  if (miss.length > 0) {
8362
- const batch = await getMarketSignals(
8363
- providerFactory("polymarket", competition),
8364
- miss,
8365
- DEFAULT_ON_MARKET_OPTS
8366
- );
8601
+ const batch = await getMarketSignals(providerFactory("polymarket", competition), miss, {
8602
+ ...DEFAULT_ON_MARKET_OPTS,
8603
+ ...clock
8604
+ });
8367
8605
  const fetched = resolvedValues(batch);
8368
8606
  complete = batch.complete;
8369
8607
  for (const id of cacheableKeys(batch)) {
8370
- marketMem.set(memKey(competition, id), { at: now, signal: fetched.get(id) ?? null });
8608
+ marketMem.set(memKey(competition, id), { at: memNow, signal: fetched.get(id) ?? null });
8371
8609
  }
8372
8610
  for (const [id, s] of fetched) result.set(id, s);
8373
8611
  }
@@ -8377,7 +8615,7 @@ async function reliableMarketData(args, matches, now) {
8377
8615
  if (!marketsEnabled()) return { data: void 0, complete: true };
8378
8616
  const relevant = matches.filter((m) => marketRelevant(m, now));
8379
8617
  if (relevant.length === 0) return { data: void 0, complete: true };
8380
- const result = await cachedMarketSignals(args, relevant);
8618
+ const result = await cachedMarketSignals(args, relevant, now);
8381
8619
  const out = {};
8382
8620
  for (const m of relevant) {
8383
8621
  const s = result.signals.get(m.id);
@@ -8531,15 +8769,17 @@ async function matchAnswer(args) {
8531
8769
  let marketSignal;
8532
8770
  let marketComplete = true;
8533
8771
  if (marketsEnabled() && marketRelevant(match, now)) {
8534
- const market = await cachedMarketSignals(args, [match]);
8772
+ const market = await cachedMarketSignals(args, [match], now);
8535
8773
  marketComplete = market.complete;
8536
8774
  const s = market.signals.get(match.id);
8537
8775
  if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(match, s)) marketSignal = s;
8538
8776
  }
8539
8777
  const attribution = dayAttribution(found, [match], args.lang);
8540
- const base = matchLine(match, opts);
8541
- const text = marketSignal ? `${base}
8542
- ${marketBlock(marketSignal, match).join("\n")}` : base;
8778
+ const text = [
8779
+ matchLine(match, opts),
8780
+ ...marketSignal ? marketBlock(marketSignal, match) : [],
8781
+ ...timelineRows(match, { lang: args.lang, flags: true })
8782
+ ].join("\n");
8543
8783
  const degradedLine = degraded ? `(${t(args.lang, found.earlierRecord ? "feed.earlierRecord" : "feed.degraded")})` : void 0;
8544
8784
  const marketLine2 = marketComplete ? void 0 : "(Market data unavailable or incomplete \u2014 this match was not checked.)";
8545
8785
  return {
@@ -8786,7 +9026,7 @@ async function marketAnswer(args) {
8786
9026
  const found = await getMatchById(adapter2, args.matchId, now2);
8787
9027
  const { match } = found;
8788
9028
  const relevant = match ? marketRelevant(match, now2) : false;
8789
- const batch2 = match && relevant ? await getMarketSignals(provider, [match], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
9029
+ const batch2 = match && relevant ? await getMarketSignals(provider, [match], { ...MARKETS_TOOL_OPTS, now: now2 }) : { results: /* @__PURE__ */ new Map(), complete: true };
8790
9030
  const sig = match ? resolvedValues(batch2).get(match.id) : void 0;
8791
9031
  const shown2 = batch2.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
8792
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);
@@ -8810,7 +9050,7 @@ async function marketAnswer(args) {
8810
9050
  const picked = await marketFixtureForTeam(adapter2, code, now2);
8811
9051
  const { match: fixture, degraded } = picked;
8812
9052
  const relevant = fixture ? marketRelevant(fixture, now2) : false;
8813
- const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
9053
+ const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], { ...MARKETS_TOOL_OPTS, now: now2 }) : { results: /* @__PURE__ */ new Map(), complete: true };
8814
9054
  const sig = fixture ? resolvedValues(batch2).get(fixture.id) : void 0;
8815
9055
  const shown2 = batch2.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
8816
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);
@@ -8835,7 +9075,7 @@ async function marketAnswer(args) {
8835
9075
  const day = await getMatchesForDate(adapter, date, resolveTz(args.tz));
8836
9076
  const fixtureRead = marketsCoverCompetition(competitionOf(args)) ? day : {};
8837
9077
  const todays = fixturesByDate(date, day.matches, args.tz).filter((m) => marketRelevant(m, now));
8838
- const batch = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
9078
+ const batch = await getMarketSignals(provider, todays, { ...MARKETS_TOOL_OPTS, now });
8839
9079
  const signals = resolvedValues(batch);
8840
9080
  const all = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
8841
9081
  (r) => !!r.signal && marketDisplayable(r.match, r.signal)
@@ -8878,7 +9118,7 @@ async function reliableSignalMap(args, matches, now) {
8878
9118
  if (!marketsEnabled()) return { signals: /* @__PURE__ */ new Map(), complete: true };
8879
9119
  const relevant = matches.filter((m) => marketRelevant(m, now));
8880
9120
  if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
8881
- const result = await cachedMarketSignals(args, relevant);
9121
+ const result = await cachedMarketSignals(args, relevant, now);
8882
9122
  const out = /* @__PURE__ */ new Map();
8883
9123
  for (const m of relevant) {
8884
9124
  const s = result.signals.get(m.id);
@@ -9071,7 +9311,7 @@ async function shareAnswer(args) {
9071
9311
 
9072
9312
  // src/server.ts
9073
9313
  var SERVER_NAME = "claudinho";
9074
- var SERVER_VERSION = "0.11.2";
9314
+ var SERVER_VERSION = "0.12.0";
9075
9315
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
9076
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.`;
9077
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.
@@ -9098,7 +9338,7 @@ var competitionArg = z.string().describe(
9098
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"
9099
9339
  );
9100
9340
  var langArg = z.string().optional().describe(
9101
- "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)"
9102
9342
  );
9103
9343
  var commonArgs = {
9104
9344
  competition: competitionArg.optional(),
@@ -9108,6 +9348,14 @@ var commonArgs = {
9108
9348
  };
9109
9349
  var teamRef = z.object({ code: z.string(), name: z.string(), flag: z.string(), id: z.string() }).partial().passthrough();
9110
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();
9111
9359
  var matchOut = z.object({
9112
9360
  id: z.string(),
9113
9361
  stage: z.string().optional(),
@@ -9123,6 +9371,9 @@ var matchOut = z.object({
9123
9371
  shootout: scorePair.optional(),
9124
9372
  status: z.string().optional(),
9125
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(),
9126
9377
  winnerCode: z.string().optional()
9127
9378
  }).passthrough();
9128
9379
  var anyObj = z.object({}).passthrough();
@@ -9541,7 +9792,7 @@ function buildServer() {
9541
9792
  "get_match",
9542
9793
  {
9543
9794
  title: "Match detail",
9544
- 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.`,
9545
9796
  inputSchema: { id: z.string().describe("Match id"), ...commonArgs },
9546
9797
  annotations: { readOnlyHint: true, openWorldHint: true },
9547
9798
  outputSchema: matchDetailOut
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.11.2",
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.31.0",
67
+ "@modelcontextprotocol/sdk": "^1.32.1",
68
68
  "zod": "^4.6.5"
69
69
  },
70
70
  "devDependencies": {
71
- "@types/node": "^22.20.4",
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.11.2"
76
+ "@claudinho/core": "0.12.0"
77
77
  },
78
78
  "scripts": {
79
79
  "build": "tsup",