cito-mcp 0.2.1 → 0.2.3
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 +2 -2
- package/dist/client.js +5 -2
- package/dist/index.js +1 -1
- package/dist/tools/live.js +129 -17
- package/dist/tools/meta.js +1 -1
- package/dist/tools/normalize.js +105 -24
- package/dist/tools/resolve.js +158 -32
- package/dist/tools/standings.js +57 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Standalone [MCP](https://modelcontextprotocol.io) server for the [Cito esports API](https://api.citoapi.com) — **curated outcome tools** for agents building esports apps, dashboards, bots, and research flows.
|
|
4
4
|
|
|
5
|
-
**Version:** `0.2.
|
|
5
|
+
**Version:** `0.2.3` · **Node:** `>=20` · **Install:** `npx cito-mcp`
|
|
6
6
|
|
|
7
7
|
Primary games: **lol · cs2 · dota2 · cod · ufc**. Fortnite and long-tail REST stay available via `call_api`.
|
|
8
8
|
|
|
@@ -398,7 +398,7 @@ src/
|
|
|
398
398
|
|
|
399
399
|
## Publish notes
|
|
400
400
|
|
|
401
|
-
Package: **`cito-mcp@0.2.
|
|
401
|
+
Package: **`cito-mcp@0.2.2`**
|
|
402
402
|
|
|
403
403
|
| Item | Value |
|
|
404
404
|
| --- | --- |
|
package/dist/client.js
CHANGED
|
@@ -137,19 +137,22 @@ export function extractRows(data) {
|
|
|
137
137
|
if (typeof data !== 'object')
|
|
138
138
|
return [];
|
|
139
139
|
const obj = data;
|
|
140
|
+
// Order matters: UFC /ufc/live nests real bouts under liveBouts while also
|
|
141
|
+
// shipping supervisor `events` (no fighter names). Prefer bout-like keys first.
|
|
140
142
|
for (const key of [
|
|
141
143
|
'data',
|
|
144
|
+
'liveBouts',
|
|
145
|
+
'bouts',
|
|
142
146
|
'matches',
|
|
143
147
|
'items',
|
|
144
148
|
'results',
|
|
145
149
|
'teams',
|
|
146
150
|
'players',
|
|
147
|
-
'events',
|
|
148
151
|
'fighters',
|
|
149
152
|
'tournaments',
|
|
150
153
|
'leagues',
|
|
151
154
|
'orgs',
|
|
152
|
-
'
|
|
155
|
+
'events',
|
|
153
156
|
'rankings',
|
|
154
157
|
'standings',
|
|
155
158
|
'rows',
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { errorEnvelope, toMcpResult } from './envelope.js';
|
|
|
18
18
|
import { SERVER_INSTRUCTIONS } from './instructions.js';
|
|
19
19
|
import { allTools, getTool } from './tools/index.js';
|
|
20
20
|
import { runTool } from './tools/types.js';
|
|
21
|
-
const PACKAGE_VERSION = '0.2.
|
|
21
|
+
const PACKAGE_VERSION = '0.2.3';
|
|
22
22
|
const API_KEY = process.env.CITO_API_KEY;
|
|
23
23
|
if (!API_KEY) {
|
|
24
24
|
console.error('[cito-mcp] CITO_API_KEY is required.\n' +
|
package/dist/tools/live.js
CHANGED
|
@@ -12,6 +12,64 @@ const LIVE_PATHS = {
|
|
|
12
12
|
cod: '/cod/matches/live',
|
|
13
13
|
ufc: '/ufc/live',
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Extract live match/bout rows. UFC /ufc/live returns
|
|
17
|
+
* { liveBouts, events, ... } — events are supervisor shells without fighters.
|
|
18
|
+
* Prefer liveBouts (and nested tracking on events) over plain events[].
|
|
19
|
+
* Exported for offline UFC projection tests.
|
|
20
|
+
*/
|
|
21
|
+
export function extractLiveRows(data, game) {
|
|
22
|
+
const root = asRecord(data);
|
|
23
|
+
const payload = asRecord(root?.data) ?? root;
|
|
24
|
+
if (!payload)
|
|
25
|
+
return extractRows(data);
|
|
26
|
+
if (game === 'ufc') {
|
|
27
|
+
// Prefer liveBouts when the key is present (including empty [] — honest empty board).
|
|
28
|
+
if (Array.isArray(payload.liveBouts)) {
|
|
29
|
+
const liveBouts = payload.liveBouts;
|
|
30
|
+
if (liveBouts.length)
|
|
31
|
+
return liveBouts;
|
|
32
|
+
// Empty liveBouts: still check nested tracking, but never bare events[]
|
|
33
|
+
const eventsWithEmptyLive = Array.isArray(payload.events)
|
|
34
|
+
? payload.events
|
|
35
|
+
: [];
|
|
36
|
+
const nested = [];
|
|
37
|
+
for (const ev of eventsWithEmptyLive) {
|
|
38
|
+
const er = asRecord(ev);
|
|
39
|
+
const tracking = Array.isArray(er?.tracking) ? er.tracking : [];
|
|
40
|
+
for (const t of tracking) {
|
|
41
|
+
const tr = asRecord(t) ?? {};
|
|
42
|
+
nested.push({
|
|
43
|
+
...tr,
|
|
44
|
+
eventSlug: pickString(tr.eventSlug, er?.eventSlug, er?.slug),
|
|
45
|
+
eventName: pickString(er?.name, er?.eventName, er?.eventSlug),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return nested; // [] when only supervisor shells
|
|
50
|
+
}
|
|
51
|
+
// liveBouts key missing — try nested tracking under events
|
|
52
|
+
const events = Array.isArray(payload.events) ? payload.events : [];
|
|
53
|
+
const fromTracking = [];
|
|
54
|
+
for (const ev of events) {
|
|
55
|
+
const er = asRecord(ev);
|
|
56
|
+
const tracking = Array.isArray(er?.tracking) ? er.tracking : [];
|
|
57
|
+
for (const t of tracking) {
|
|
58
|
+
const tr = asRecord(t) ?? {};
|
|
59
|
+
fromTracking.push({
|
|
60
|
+
...tr,
|
|
61
|
+
eventSlug: pickString(tr.eventSlug, er?.eventSlug, er?.slug),
|
|
62
|
+
eventName: pickString(er?.name, er?.eventName, er?.eventSlug),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (fromTracking.length)
|
|
67
|
+
return fromTracking;
|
|
68
|
+
// Last resort: empty live board rather than fake ? vs ? event shells
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return extractRows(data);
|
|
72
|
+
}
|
|
15
73
|
export const liveMatches = {
|
|
16
74
|
name: 'live_matches',
|
|
17
75
|
description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
|
|
@@ -88,12 +146,25 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
88
146
|
});
|
|
89
147
|
continue;
|
|
90
148
|
}
|
|
91
|
-
//
|
|
92
|
-
let rows =
|
|
149
|
+
// Prefer real match/bout rows over supervisor event shells (UFC /ufc/live).
|
|
150
|
+
let rows = extractLiveRows(res.data, game);
|
|
93
151
|
const obj = asRecord(res.data);
|
|
94
152
|
if (rows.length === 0 && Array.isArray(obj?.matches))
|
|
95
153
|
rows = obj.matches;
|
|
96
|
-
const normalized = rows
|
|
154
|
+
const normalized = rows
|
|
155
|
+
.slice(0, limitPerGame)
|
|
156
|
+
.map((row) => normalizeMatch(game, row, 'live'))
|
|
157
|
+
// Drop hollow UFC event-supervisor rows mistaken for bouts (no bout id + no sides).
|
|
158
|
+
.filter((m) => {
|
|
159
|
+
if (game !== 'ufc')
|
|
160
|
+
return true;
|
|
161
|
+
if (m.matchId && m.matchId !== 'unknown' && (m.team1?.name || m.team2?.name))
|
|
162
|
+
return true;
|
|
163
|
+
if (m.team1?.name && m.team2?.name)
|
|
164
|
+
return true;
|
|
165
|
+
// Keep if at least a real bout id even when fighters pending
|
|
166
|
+
return Boolean(m.matchId && m.matchId !== 'unknown' && !String(m.matchId).startsWith('event'));
|
|
167
|
+
});
|
|
97
168
|
const items = labelsOnly
|
|
98
169
|
? normalized.map((m) => ({
|
|
99
170
|
game: m.game,
|
|
@@ -143,7 +214,8 @@ Filter support (unsupported params are ignored with meta.warnings — do not ass
|
|
|
143
214
|
- lol: hours, team (slug), league (slug)
|
|
144
215
|
- cs2: team, from, to (ISO); hours not applied upstream
|
|
145
216
|
- cod: team, tournamentId
|
|
146
|
-
- dota2
|
|
217
|
+
- dota2: limit/cursor primarily; team may be client-filtered where data allows
|
|
218
|
+
- ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
|
|
147
219
|
|
|
148
220
|
Parallel-safe: yes. Upstream cost: 1–2.
|
|
149
221
|
Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
@@ -265,19 +337,19 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
265
337
|
}
|
|
266
338
|
else if (game === 'ufc') {
|
|
267
339
|
path = '/ufc/events/upcoming';
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
340
|
+
// Request more events than limit so client-side hours filter still has a card pool.
|
|
341
|
+
query = {
|
|
342
|
+
limit: Math.min(50, Math.max(limit * 3, limit)),
|
|
343
|
+
page: Math.floor(offset / limit) + 1,
|
|
344
|
+
includeBouts: true,
|
|
345
|
+
};
|
|
346
|
+
// hours / from / to applied client-side after expand (below) — do not warn as ignored.
|
|
271
347
|
if (team)
|
|
272
348
|
noteIgnored('team', 'UFC uses fighter filters via resolve/event_card, not team on schedule');
|
|
273
349
|
if (league)
|
|
274
350
|
noteIgnored('league', 'UFC has no league filter on upcoming events');
|
|
275
351
|
if (tournamentId)
|
|
276
352
|
noteIgnored('tournamentId', 'pass event slug via event_card instead');
|
|
277
|
-
if (from)
|
|
278
|
-
noteIgnored('from', 'UFC upcoming does not accept from');
|
|
279
|
-
if (to)
|
|
280
|
-
noteIgnored('to', 'UFC upcoming does not accept to');
|
|
281
353
|
}
|
|
282
354
|
const res = await fetchJson(ctx, path, { query });
|
|
283
355
|
if (!res.ok) {
|
|
@@ -294,29 +366,69 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
294
366
|
});
|
|
295
367
|
}
|
|
296
368
|
let rows = extractRows(res.data);
|
|
297
|
-
// UFC events
|
|
369
|
+
// UFC events → expand to bout rows (fighters[] shape) so labels are not "? vs ?"
|
|
298
370
|
if (game === 'ufc') {
|
|
299
371
|
const expanded = [];
|
|
300
372
|
for (const event of rows) {
|
|
301
373
|
const er = asRecord(event) ?? {};
|
|
374
|
+
const eventName = pickString(er.name, er.title);
|
|
375
|
+
const eventId = pickString(er.id, er.slug);
|
|
376
|
+
const eventSlug = pickString(er.slug);
|
|
377
|
+
const eventStart = pickString(er.startTime, er.date, er.startsAt);
|
|
302
378
|
const bouts = extractRows(er.bouts ?? er.fights);
|
|
303
379
|
if (bouts.length) {
|
|
304
380
|
for (const bout of bouts) {
|
|
305
381
|
const br = asRecord(bout) ?? {};
|
|
306
382
|
expanded.push({
|
|
307
383
|
...br,
|
|
308
|
-
eventName
|
|
309
|
-
eventId
|
|
310
|
-
eventSlug
|
|
311
|
-
startTime: pickString(br.startTime,
|
|
384
|
+
eventName,
|
|
385
|
+
eventId,
|
|
386
|
+
eventSlug,
|
|
387
|
+
startTime: pickString(br.startTime, br.date, eventStart),
|
|
312
388
|
});
|
|
313
389
|
}
|
|
314
390
|
}
|
|
315
391
|
else {
|
|
316
|
-
|
|
392
|
+
// Keep event shell as a schedule card with a real label (not fighter matchup)
|
|
393
|
+
expanded.push({
|
|
394
|
+
id: eventId ?? eventSlug,
|
|
395
|
+
matchId: eventId ?? eventSlug,
|
|
396
|
+
name: eventName,
|
|
397
|
+
title: eventName,
|
|
398
|
+
label: eventName ?? eventSlug ?? 'UFC event',
|
|
399
|
+
startTime: eventStart,
|
|
400
|
+
eventName,
|
|
401
|
+
eventId,
|
|
402
|
+
eventSlug,
|
|
403
|
+
status: pickString(er.status) ?? 'upcoming',
|
|
404
|
+
});
|
|
317
405
|
}
|
|
318
406
|
}
|
|
319
407
|
rows = expanded;
|
|
408
|
+
// Client-side hours / from / to window (API list has no hours param)
|
|
409
|
+
const now = Date.now();
|
|
410
|
+
const fromMs = from ? Date.parse(from) : now;
|
|
411
|
+
const toMs = to
|
|
412
|
+
? Date.parse(to)
|
|
413
|
+
: args.hours !== undefined || !from
|
|
414
|
+
? now + hours * 3600_000
|
|
415
|
+
: Number.POSITIVE_INFINITY;
|
|
416
|
+
if (Number.isFinite(fromMs) && Number.isFinite(toMs)) {
|
|
417
|
+
const before = rows.length;
|
|
418
|
+
rows = rows.filter((row) => {
|
|
419
|
+
const r = asRecord(row) ?? {};
|
|
420
|
+
const ts = pickString(r.startTime, r.date, r.startsAt, r.scheduledAt);
|
|
421
|
+
if (!ts)
|
|
422
|
+
return true; // keep undated rows rather than drop whole card
|
|
423
|
+
const t = Date.parse(ts);
|
|
424
|
+
if (!Number.isFinite(t))
|
|
425
|
+
return true;
|
|
426
|
+
return t >= fromMs && t <= toMs;
|
|
427
|
+
});
|
|
428
|
+
if (before > 0 && rows.length === 0) {
|
|
429
|
+
warnings.push(`hours/from/to window matched 0 of ${before} UFC rows — widen hours or omit time filters`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
320
432
|
}
|
|
321
433
|
// Client-side team filter when API ignored it
|
|
322
434
|
if (team && (game === 'dota2' || game === 'cs2')) {
|
package/dist/tools/meta.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { extractRows, fetchJson, gameNotIncludedHint, present } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { boolSchema, gameSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
7
|
-
const CATALOG_VERSION = '0.2.
|
|
7
|
+
const CATALOG_VERSION = '0.2.3';
|
|
8
8
|
const TOOL_CATALOG = [
|
|
9
9
|
{
|
|
10
10
|
name: 'list_capabilities',
|
package/dist/tools/normalize.js
CHANGED
|
@@ -19,9 +19,10 @@ function nestedSide(raw) {
|
|
|
19
19
|
return { name: raw };
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
22
|
+
const profile = asRecord(o.profile);
|
|
23
|
+
const name = pickString(o.name, o.fighterName, o.code, o.shortName, o.nickname, o.displayName, profile?.name, profile?.nickname);
|
|
24
|
+
const id = pickString(o.id, o.teamId, o.orgId, o.fighterId, profile?.id);
|
|
25
|
+
const slug = pickString(o.slug, o.orgSlug, o.fighterSlug, profile?.slug);
|
|
25
26
|
const scoreRaw = o.score ?? o.mapsWon ?? o.gamesWon;
|
|
26
27
|
const score = typeof scoreRaw === 'number'
|
|
27
28
|
? scoreRaw
|
|
@@ -39,17 +40,56 @@ function scoreNum(v) {
|
|
|
39
40
|
}
|
|
40
41
|
export function normalizeMatch(game, row, forcedStatus) {
|
|
41
42
|
const r = asRecord(row) ?? {};
|
|
42
|
-
const matchId = pickString(r.matchId, r.boutId, r.id, r.gameId, r.match_id) ?? 'unknown';
|
|
43
|
+
const matchId = pickString(r.matchId, r.boutId, r.id, r.gameId, r.match_id, r.dataId, r.fightMetricId) ?? 'unknown';
|
|
43
44
|
let team1 = nestedSide(r.team1) ??
|
|
44
45
|
sideFrom(pickString(r.team1Name, r.team_a_name, r.redName, r.fighter1Name, r.homeName), pickString(r.team1Id, r.team1_id, r.redId, r.fighter1Id), pickString(r.team1Slug, r.redSlug, r.fighter1Slug), scoreNum(r.team1Score ?? r.score1 ?? r.team1Maps ?? r.redScore));
|
|
45
46
|
let team2 = nestedSide(r.team2) ??
|
|
46
47
|
sideFrom(pickString(r.team2Name, r.team_b_name, r.blueName, r.fighter2Name, r.awayName), pickString(r.team2Id, r.team2_id, r.blueId, r.fighter2Id), pickString(r.team2Slug, r.blueSlug, r.fighter2Slug), scoreNum(r.team2Score ?? r.score2 ?? r.team2Maps ?? r.blueScore));
|
|
47
|
-
// UFC fighters
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// UFC / live corners: red/blue objects, corner arrays, or fighters[] with corner field.
|
|
49
|
+
// Public API serializeBout uses fighters:[{ corner, fighterName, fighterSlug, profile:{name,slug} }].
|
|
50
|
+
// Live tracking uses red/blue + fighters[] (not team1/team2).
|
|
51
|
+
if (game === 'ufc' || (!team1 && !team2 && (r.red || r.blue || Array.isArray(r.fighters)))) {
|
|
52
|
+
const fromCornerObj = (raw) => {
|
|
53
|
+
const o = asRecord(raw);
|
|
54
|
+
if (!o) {
|
|
55
|
+
if (typeof raw === 'string' && raw.trim())
|
|
56
|
+
return { name: raw.trim() };
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// Nested profile from serializeBoutFighter
|
|
60
|
+
const profile = asRecord(o.profile);
|
|
61
|
+
const fighter = asRecord(o.fighter);
|
|
62
|
+
const name = pickString(o.name, o.fighterName, o.displayName, o.nickname, profile?.name, profile?.nickname, fighter?.name, fighter?.nickname);
|
|
63
|
+
const id = pickString(o.id, o.fighterId, profile?.id, fighter?.id);
|
|
64
|
+
const slug = pickString(o.slug, o.fighterSlug, profile?.slug, fighter?.slug);
|
|
65
|
+
const score = scoreNum(o.score ?? o.points);
|
|
66
|
+
return sideFrom(name, id, slug, score);
|
|
67
|
+
};
|
|
68
|
+
team1 =
|
|
69
|
+
team1 ??
|
|
70
|
+
fromCornerObj(r.red) ??
|
|
71
|
+
fromCornerObj(r.redCorner) ??
|
|
72
|
+
fromCornerObj(r.fighter1) ??
|
|
73
|
+
fromCornerObj(r.fighterA);
|
|
74
|
+
team2 =
|
|
75
|
+
team2 ??
|
|
76
|
+
fromCornerObj(r.blue) ??
|
|
77
|
+
fromCornerObj(r.blueCorner) ??
|
|
78
|
+
fromCornerObj(r.fighter2) ??
|
|
79
|
+
fromCornerObj(r.fighterB);
|
|
80
|
+
const fightersList = Array.isArray(r.fighters) ? r.fighters : [];
|
|
81
|
+
if (fightersList.length) {
|
|
82
|
+
const byCorner = (want) => fightersList.find((f) => {
|
|
83
|
+
const c = pickString(asRecord(f)?.corner, asRecord(f)?.side)?.toLowerCase();
|
|
84
|
+
return c === want || c === want[0]; // "red" / "r"
|
|
85
|
+
});
|
|
86
|
+
const redF = byCorner('red') ?? byCorner('r') ?? fightersList[0];
|
|
87
|
+
const blueF = byCorner('blue') ??
|
|
88
|
+
byCorner('b') ??
|
|
89
|
+
(fightersList.length > 1 ? fightersList[1] : undefined);
|
|
90
|
+
team1 = team1 ?? fromCornerObj(redF);
|
|
91
|
+
team2 = team2 ?? fromCornerObj(blueF);
|
|
92
|
+
}
|
|
53
93
|
}
|
|
54
94
|
// COD sometimes uses teams[]
|
|
55
95
|
if ((!team1 || !team2) && Array.isArray(r.teams)) {
|
|
@@ -75,8 +115,21 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
75
115
|
const leagueName = pickString(asRecord(r.league)?.name, r.leagueName, r.league, r.leagueSlug);
|
|
76
116
|
const leagueId = pickString(asRecord(r.league)?.id, r.leagueId);
|
|
77
117
|
const leagueSlug = pickString(asRecord(r.league)?.slug, r.leagueSlug);
|
|
78
|
-
|
|
79
|
-
|
|
118
|
+
// Prefer explicit title/label when present, but never keep the placeholder
|
|
119
|
+
// "? vs ?" once fighter/team names were resolved (UFC fighters[] / red-blue).
|
|
120
|
+
const vsLabel = `${team1?.name ?? '?'} vs ${team2?.name ?? '?'}`;
|
|
121
|
+
const explicit = pickString(r.label, r.title, r.name);
|
|
122
|
+
const explicitIsPlaceholder = explicit != null && /^\?\s*vs\s*\?$/i.test(explicit.trim());
|
|
123
|
+
let label;
|
|
124
|
+
if (explicit && !explicitIsPlaceholder) {
|
|
125
|
+
label = explicit;
|
|
126
|
+
}
|
|
127
|
+
else if (team1?.name || team2?.name) {
|
|
128
|
+
label = vsLabel;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
label = explicit ?? vsLabel;
|
|
132
|
+
}
|
|
80
133
|
return {
|
|
81
134
|
game,
|
|
82
135
|
matchId,
|
|
@@ -96,6 +149,7 @@ export function entityRef(row, type, game) {
|
|
|
96
149
|
'unknown';
|
|
97
150
|
const slug = pickString(r.slug, r.orgSlug, r.teamSlug);
|
|
98
151
|
const name = pickString(r.name, r.nickname, r.displayName, r.tag, r.code, r.title, slug, id) ?? id;
|
|
152
|
+
const nickname = pickString(r.nickname, asRecord(r.profile)?.nickname);
|
|
99
153
|
return {
|
|
100
154
|
game,
|
|
101
155
|
type,
|
|
@@ -103,34 +157,61 @@ export function entityRef(row, type, game) {
|
|
|
103
157
|
...(slug ? { slug } : {}),
|
|
104
158
|
name,
|
|
105
159
|
meta: {
|
|
160
|
+
...(nickname && nickname !== name ? { nickname } : {}),
|
|
106
161
|
...(r.region ? { region: r.region } : {}),
|
|
107
162
|
...(r.role ? { role: r.role } : {}),
|
|
108
163
|
...(r.nationality ? { nationality: r.nationality } : {}),
|
|
109
164
|
...(r.entity_type ? { entity_type: r.entity_type } : {}),
|
|
165
|
+
...(r.division ? { division: r.division } : {}),
|
|
166
|
+
...(r.p4pRank != null ? { p4pRank: r.p4pRank } : {}),
|
|
167
|
+
...(r.isChampion != null ? { isChampion: r.isChampion } : {}),
|
|
110
168
|
},
|
|
111
169
|
};
|
|
112
170
|
}
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Fuzzy score for entity resolution: exact > startsWith > includes > token overlap.
|
|
173
|
+
* Extra args (nickname, alt names) are scored as additional candidates.
|
|
174
|
+
* Multi-token person names ("jon jones") get a strong boost when every token hits.
|
|
175
|
+
*/
|
|
176
|
+
export function rankScore(query, name, id, slug, ...extra) {
|
|
177
|
+
const q = query.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
116
178
|
if (!q)
|
|
117
179
|
return 0;
|
|
118
|
-
const candidates = [name, id, slug]
|
|
180
|
+
const candidates = [name, id, slug, ...extra]
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.map((s) => String(s).toLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim());
|
|
119
183
|
let best = 0;
|
|
184
|
+
const qt = q.split(/\s+/).filter(Boolean);
|
|
120
185
|
for (const c of candidates) {
|
|
186
|
+
if (!c)
|
|
187
|
+
continue;
|
|
121
188
|
if (c === q)
|
|
122
189
|
best = Math.max(best, 100);
|
|
123
190
|
else if (c.startsWith(q))
|
|
124
|
-
best = Math.max(best,
|
|
191
|
+
best = Math.max(best, 85 - Math.min(15, c.length - q.length));
|
|
192
|
+
else if (q.startsWith(c) && c.length >= 3)
|
|
193
|
+
best = Math.max(best, 70);
|
|
125
194
|
else if (c.includes(q))
|
|
126
|
-
best = Math.max(best,
|
|
195
|
+
best = Math.max(best, 55 - Math.min(20, c.indexOf(q)));
|
|
127
196
|
else {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
best = Math.max(best,
|
|
197
|
+
const ct = c.split(/[\s]+/).filter(Boolean);
|
|
198
|
+
// all query tokens present as whole tokens (order-independent)
|
|
199
|
+
const allTokens = qt.length > 0 &&
|
|
200
|
+
qt.every((t) => ct.some((x) => x === t || x.startsWith(t) || t.startsWith(x)));
|
|
201
|
+
if (allTokens && qt.length >= 2) {
|
|
202
|
+
best = Math.max(best, 92); // "jon jones" vs "Jon Jones"
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const hits = qt.filter((t) => ct.some((x) => x.includes(t) || t.includes(x))).length;
|
|
206
|
+
if (hits === qt.length && qt.length > 0)
|
|
207
|
+
best = Math.max(best, 75);
|
|
208
|
+
else if (hits)
|
|
209
|
+
best = Math.max(best, 20 + hits * 12);
|
|
210
|
+
}
|
|
211
|
+
// last-name only: query last token equals candidate last token
|
|
212
|
+
if (qt.length >= 2 && ct.length >= 1 && ct[ct.length - 1] === qt[qt.length - 1]) {
|
|
213
|
+
best = Math.max(best, 60);
|
|
214
|
+
}
|
|
134
215
|
}
|
|
135
216
|
}
|
|
136
217
|
return best;
|
package/dist/tools/resolve.js
CHANGED
|
@@ -8,31 +8,66 @@ import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, PRIMARY_
|
|
|
8
8
|
function pushCandidates(out, game, type, rows, q, limit) {
|
|
9
9
|
for (const row of rows) {
|
|
10
10
|
const ref = entityRef(row, type, game);
|
|
11
|
-
const score = rankScore(q, ref.name, ref.id, ref.slug);
|
|
12
|
-
if (q && score <= 0 && type !== 'any') {
|
|
13
|
-
// still include search API hits even if local rank is weak
|
|
14
|
-
}
|
|
15
11
|
const r = asRecord(row) ?? {};
|
|
12
|
+
const nickname = pickString(r.nickname, asRecord(r.profile)?.nickname, ref.meta?.nickname);
|
|
13
|
+
const score = rankScore(q, ref.name, ref.id, ref.slug, nickname);
|
|
14
|
+
// Prefer positive fuzzy hits; keep weak API hits at floor 1 so real search results are not wiped.
|
|
15
|
+
if (q && score <= 0) {
|
|
16
|
+
// still allow through at floor so dedicated search endpoints aren't empty on odd nicknames
|
|
17
|
+
}
|
|
16
18
|
const secondary = {};
|
|
17
19
|
for (const key of ['lolPlayerId', 'codPlayerId', 'matchId', 'boutId', 'tournamentId', 'eventId']) {
|
|
18
20
|
const v = pickString(r[key]);
|
|
19
21
|
if (v)
|
|
20
22
|
secondary[key] = v;
|
|
21
23
|
}
|
|
24
|
+
if (nickname)
|
|
25
|
+
secondary.nickname = nickname;
|
|
22
26
|
out.push({
|
|
23
27
|
game,
|
|
24
28
|
type: pickString(r.entity_type, r.type, type) ?? type,
|
|
25
29
|
id: ref.id,
|
|
26
30
|
slug: ref.slug,
|
|
27
31
|
name: ref.name,
|
|
28
|
-
score: q ? (score
|
|
32
|
+
score: q ? (score > 0 ? score : 1) : 10,
|
|
29
33
|
...(Object.keys(secondary).length ? { secondaryIds: secondary } : {}),
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
|
-
out.sort((a, b) => b.score - a.score);
|
|
36
|
+
out.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
33
37
|
if (out.length > limit * 3)
|
|
34
38
|
out.length = limit * 3;
|
|
35
39
|
}
|
|
40
|
+
/** UFC dedicated search (list /fighters ignores `q`). */
|
|
41
|
+
async function ufcSearch(ctx, q, type, limit) {
|
|
42
|
+
const candidates = [];
|
|
43
|
+
const res = await fetchJson(ctx, '/ufc/search', { query: { q } });
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
return { candidates, calls: 1, ok: false, status: res.status };
|
|
46
|
+
}
|
|
47
|
+
const data = asRecord(res.data) ?? {};
|
|
48
|
+
// withMeta nests under data; extractRows may return fighters only — read buckets explicitly
|
|
49
|
+
const payload = asRecord(data.data) ?? data;
|
|
50
|
+
const fighters = Array.isArray(payload.fighters)
|
|
51
|
+
? payload.fighters
|
|
52
|
+
: extractRows(payload.fighters);
|
|
53
|
+
const events = Array.isArray(payload.events)
|
|
54
|
+
? payload.events
|
|
55
|
+
: extractRows(payload.events);
|
|
56
|
+
const bouts = Array.isArray(payload.bouts)
|
|
57
|
+
? payload.bouts
|
|
58
|
+
: extractRows(payload.bouts);
|
|
59
|
+
if (type === 'any' || type === 'fighter' || type === 'player') {
|
|
60
|
+
pushCandidates(candidates, 'ufc', 'fighter', fighters, q, limit);
|
|
61
|
+
}
|
|
62
|
+
if (type === 'any' || type === 'event' || type === 'tournament') {
|
|
63
|
+
pushCandidates(candidates, 'ufc', 'event', events, q, limit);
|
|
64
|
+
}
|
|
65
|
+
if (type === 'any' || type === 'match') {
|
|
66
|
+
pushCandidates(candidates, 'ufc', 'match', bouts, q, limit);
|
|
67
|
+
}
|
|
68
|
+
candidates.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
69
|
+
return { candidates: candidates.slice(0, limit), calls: 1, ok: true };
|
|
70
|
+
}
|
|
36
71
|
async function searchGame(ctx, game, q, type, limit) {
|
|
37
72
|
const candidates = [];
|
|
38
73
|
let calls = 0;
|
|
@@ -164,24 +199,45 @@ async function searchGame(ctx, game, q, type, limit) {
|
|
|
164
199
|
await Promise.all(tasks);
|
|
165
200
|
}
|
|
166
201
|
else if (game === 'ufc') {
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
202
|
+
// /ufc/fighters ignores q (returns p4p/champ list). Always use /ufc/search when querying.
|
|
203
|
+
if (q) {
|
|
204
|
+
const ufc = await ufcSearch(ctx, q, type, limit);
|
|
205
|
+
calls += ufc.calls;
|
|
206
|
+
if (ufc.ok) {
|
|
207
|
+
candidates.push(...ufc.candidates);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
return {
|
|
211
|
+
candidates,
|
|
212
|
+
calls,
|
|
213
|
+
error: partialFromRejection(`game:${game}`, {
|
|
214
|
+
code: mapHttpToCode(ufc.status ?? 0),
|
|
215
|
+
message: `ufc search HTTP ${ufc.status ?? 0}`,
|
|
216
|
+
httpStatus: ufc.status,
|
|
217
|
+
}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
175
220
|
}
|
|
176
|
-
|
|
177
|
-
tasks
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
221
|
+
else {
|
|
222
|
+
const tasks = [];
|
|
223
|
+
if (type === 'any' || type === 'fighter' || type === 'player') {
|
|
224
|
+
tasks.push((async () => {
|
|
225
|
+
const res = await fetchJson(ctx, '/ufc/fighters', { query: { limit, page: 1 } });
|
|
226
|
+
calls += 1;
|
|
227
|
+
if (res.ok)
|
|
228
|
+
pushCandidates(candidates, game, 'fighter', extractRows(res.data), q, limit);
|
|
229
|
+
})());
|
|
230
|
+
}
|
|
231
|
+
if (type === 'any' || type === 'event' || type === 'tournament') {
|
|
232
|
+
tasks.push((async () => {
|
|
233
|
+
const res = await fetchJson(ctx, '/ufc/events', { query: { limit, page: 1 } });
|
|
234
|
+
calls += 1;
|
|
235
|
+
if (res.ok)
|
|
236
|
+
pushCandidates(candidates, game, 'event', extractRows(res.data), q, limit);
|
|
237
|
+
})());
|
|
238
|
+
}
|
|
239
|
+
await Promise.all(tasks);
|
|
183
240
|
}
|
|
184
|
-
await Promise.all(tasks);
|
|
185
241
|
}
|
|
186
242
|
}
|
|
187
243
|
catch (e) {
|
|
@@ -312,14 +368,17 @@ When to use:
|
|
|
312
368
|
- Typeahead / pickers
|
|
313
369
|
- "List teams matching…"
|
|
314
370
|
- Exploring entities without committing to one ID
|
|
371
|
+
- UFC fighter lookup by name/nickname (uses /ufc/search + client re-rank)
|
|
315
372
|
|
|
316
373
|
Prefer over resolve_entity when the user wants a list.
|
|
317
374
|
Prefer resolve_entity when chaining one name into a profile tool.
|
|
318
375
|
|
|
319
376
|
Do not use when: fetching a known entity profile — use team_profile or player_profile.
|
|
320
377
|
|
|
378
|
+
UFC: with q set, results are ranked (exact name > multi-token match > nickname). "Jon Jones" should return jon-jones first — never the generic P4P list.
|
|
379
|
+
|
|
321
380
|
Parallel-safe: yes. Upstream cost: 1–3.
|
|
322
|
-
Example: { "game": "
|
|
381
|
+
Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
323
382
|
inputSchema: {
|
|
324
383
|
type: 'object',
|
|
325
384
|
additionalProperties: false,
|
|
@@ -466,11 +525,69 @@ Example: { "game": "cs2", "q": "vitality", "type": "team", "limit": 20 }`,
|
|
|
466
525
|
}
|
|
467
526
|
}
|
|
468
527
|
else if (game === 'ufc') {
|
|
469
|
-
if (
|
|
470
|
-
|
|
528
|
+
if (q) {
|
|
529
|
+
// Dedicated search ranks name/slug/nickname; /fighters ignores q.
|
|
530
|
+
const res = await fetchJson(ctx, '/ufc/search', { query: { q } });
|
|
531
|
+
upstreamCalls += 1;
|
|
532
|
+
if (!res.ok) {
|
|
533
|
+
throw Object.assign(new Error(`HTTP ${res.status}`), {
|
|
534
|
+
status: res.status,
|
|
535
|
+
data: res.data,
|
|
536
|
+
headers: res.headers,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const data = asRecord(res.data) ?? {};
|
|
540
|
+
const payload = asRecord(data.data) ?? data;
|
|
541
|
+
const scored = [];
|
|
542
|
+
if (type === 'fighter' || type === 'player' || type === 'any') {
|
|
543
|
+
const fighters = Array.isArray(payload.fighters)
|
|
544
|
+
? payload.fighters
|
|
545
|
+
: extractRows(payload.fighters);
|
|
546
|
+
for (const row of fighters) {
|
|
547
|
+
const ref = entityRef(row, 'fighter', game);
|
|
548
|
+
const r = asRecord(row) ?? {};
|
|
549
|
+
const nick = pickString(r.nickname);
|
|
550
|
+
const score = rankScore(q, ref.name, ref.id, ref.slug, nick);
|
|
551
|
+
if (score > 0)
|
|
552
|
+
scored.push({ ref, score });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (type === 'event' || type === 'tournament' || type === 'any') {
|
|
556
|
+
const events = Array.isArray(payload.events)
|
|
557
|
+
? payload.events
|
|
558
|
+
: extractRows(payload.events);
|
|
559
|
+
for (const row of events) {
|
|
560
|
+
const ref = entityRef(row, 'event', game);
|
|
561
|
+
const score = rankScore(q, ref.name, ref.id, ref.slug);
|
|
562
|
+
if (score > 0)
|
|
563
|
+
scored.push({ ref, score });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (type === 'match' || type === 'any') {
|
|
567
|
+
const bouts = Array.isArray(payload.bouts)
|
|
568
|
+
? payload.bouts
|
|
569
|
+
: extractRows(payload.bouts);
|
|
570
|
+
for (const row of bouts) {
|
|
571
|
+
const ref = entityRef(row, 'match', game);
|
|
572
|
+
const score = rankScore(q, ref.name, ref.id, ref.slug);
|
|
573
|
+
if (score > 0)
|
|
574
|
+
scored.push({ ref, score });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
scored.sort((a, b) => b.score - a.score || a.ref.name.localeCompare(b.ref.name));
|
|
578
|
+
items = scored.map((s) => ({
|
|
579
|
+
...s.ref,
|
|
580
|
+
meta: { ...s.ref.meta, score: s.score },
|
|
581
|
+
}));
|
|
582
|
+
total = items.length;
|
|
471
583
|
}
|
|
472
|
-
|
|
473
|
-
|
|
584
|
+
else {
|
|
585
|
+
if (type === 'fighter' || type === 'player' || type === 'any') {
|
|
586
|
+
await listPath('/ufc/fighters', 'fighter');
|
|
587
|
+
}
|
|
588
|
+
if (type === 'event' || type === 'tournament' || type === 'any') {
|
|
589
|
+
await listPath('/ufc/events', 'event');
|
|
590
|
+
}
|
|
474
591
|
}
|
|
475
592
|
}
|
|
476
593
|
}
|
|
@@ -487,7 +604,7 @@ Example: { "game": "cs2", "q": "vitality", "type": "team", "limit": 20 }`,
|
|
|
487
604
|
rateLimit: e.headers,
|
|
488
605
|
});
|
|
489
606
|
}
|
|
490
|
-
// de-dupe by game+type+id
|
|
607
|
+
// de-dupe by game+type+id; when q present re-sort by score if available
|
|
491
608
|
const seen = new Set();
|
|
492
609
|
items = items.filter((it) => {
|
|
493
610
|
const key = `${it.game}:${it.type}:${it.id}`;
|
|
@@ -495,8 +612,17 @@ Example: { "game": "cs2", "q": "vitality", "type": "team", "limit": 20 }`,
|
|
|
495
612
|
return false;
|
|
496
613
|
seen.add(key);
|
|
497
614
|
return true;
|
|
498
|
-
})
|
|
499
|
-
|
|
615
|
+
});
|
|
616
|
+
if (q) {
|
|
617
|
+
items.sort((a, b) => {
|
|
618
|
+
const sa = typeof a.meta?.score === 'number' ? a.meta.score : rankScore(q, a.name, a.id, a.slug, a.meta?.nickname);
|
|
619
|
+
const sb = typeof b.meta?.score === 'number' ? b.meta.score : rankScore(q, b.name, b.id, b.slug, b.meta?.nickname);
|
|
620
|
+
return sb - sa || a.name.localeCompare(b.name);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
// pagination over ranked result set
|
|
624
|
+
const pageItems = items.slice(offset, offset + limit);
|
|
625
|
+
const hasMore = offset + pageItems.length < items.length || (!q && items.length >= limit);
|
|
500
626
|
return successEnvelope({
|
|
501
627
|
source: 'search_entities',
|
|
502
628
|
game,
|
|
@@ -506,12 +632,12 @@ Example: { "game": "cs2", "q": "vitality", "type": "team", "limit": 20 }`,
|
|
|
506
632
|
pagination: {
|
|
507
633
|
limit,
|
|
508
634
|
offset,
|
|
509
|
-
total,
|
|
635
|
+
total: total ?? (q ? items.length : null),
|
|
510
636
|
hasMore,
|
|
511
637
|
nextCursor: hasMore ? encodeCursor({ offset: offset + limit }) : null,
|
|
512
638
|
prevCursor: offset > 0 ? encodeCursor({ offset: Math.max(0, offset - limit) }) : null,
|
|
513
639
|
},
|
|
514
|
-
data: { items },
|
|
640
|
+
data: { items: pageItems },
|
|
515
641
|
});
|
|
516
642
|
},
|
|
517
643
|
};
|
package/dist/tools/standings.js
CHANGED
|
@@ -4,15 +4,66 @@
|
|
|
4
4
|
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
7
|
-
|
|
7
|
+
/** Exported for offline UFC rank mapping tests. */
|
|
8
|
+
export function normalizeStandingRow(row, index) {
|
|
8
9
|
const r = asRecord(row) ?? {};
|
|
9
10
|
const entity = asRecord(r.team) ?? asRecord(r.fighter) ?? asRecord(r.org) ?? r;
|
|
10
11
|
const name = pickString(asRecord(entity)?.name, r.teamName, r.name, r.orgName, r.fighterName, asRecord(entity)?.slug) ?? `row-${index + 1}`;
|
|
12
|
+
// UFC official lists: champion has rank=null + rankText="C" (interim "IC"); contenders 1..15.
|
|
13
|
+
// Never fall back to index+1 for explicit null ranks — that produced two "#1" rows (champ + #1).
|
|
14
|
+
const championStatus = pickString(r.championStatus, asRecord(entity)?.championStatus);
|
|
15
|
+
const rankText = pickString(r.rankText);
|
|
16
|
+
const isInterim = championStatus === 'interim' || rankText === 'IC' || r.isInterimChampion === true;
|
|
17
|
+
const isChampion = championStatus === 'champion' ||
|
|
18
|
+
rankText === 'C' ||
|
|
19
|
+
r.isChampion === true ||
|
|
20
|
+
r.isTitleHolder === true ||
|
|
21
|
+
isInterim;
|
|
22
|
+
let rank;
|
|
23
|
+
if (isChampion) {
|
|
24
|
+
rank = isInterim || rankText === 'IC' ? 'IC' : 'C';
|
|
25
|
+
}
|
|
26
|
+
else if (typeof r.rank === 'number') {
|
|
27
|
+
rank = r.rank;
|
|
28
|
+
}
|
|
29
|
+
else if (typeof r.position === 'number') {
|
|
30
|
+
rank = r.position;
|
|
31
|
+
}
|
|
32
|
+
else if (rankText && /^\d+$/.test(rankText)) {
|
|
33
|
+
rank = Number(rankText);
|
|
34
|
+
}
|
|
35
|
+
else if (r.rank === null) {
|
|
36
|
+
// Explicit null (UFC champ without flags, or unranked) — never invent index+1
|
|
37
|
+
rank = rankText ?? null;
|
|
38
|
+
}
|
|
39
|
+
else if (rankText) {
|
|
40
|
+
rank = rankText;
|
|
41
|
+
}
|
|
42
|
+
else if (r.rank === undefined && r.position === undefined) {
|
|
43
|
+
rank = index + 1; // last resort only when rank fields are absent entirely
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
rank = null;
|
|
47
|
+
}
|
|
48
|
+
const resolvedChampionStatus = isInterim
|
|
49
|
+
? 'interim'
|
|
50
|
+
: isChampion
|
|
51
|
+
? 'champion'
|
|
52
|
+
: championStatus === 'interim'
|
|
53
|
+
? 'interim'
|
|
54
|
+
: 'none';
|
|
11
55
|
return {
|
|
12
|
-
rank
|
|
56
|
+
rank,
|
|
57
|
+
rankText: rankText ??
|
|
58
|
+
(rank === 'C' || rank === 'IC'
|
|
59
|
+
? String(rank)
|
|
60
|
+
: typeof rank === 'number' || typeof rank === 'string'
|
|
61
|
+
? String(rank)
|
|
62
|
+
: null),
|
|
63
|
+
championStatus: resolvedChampionStatus,
|
|
13
64
|
teamOrFighter: {
|
|
14
65
|
id: pickString(asRecord(entity)?.id, r.teamId, r.fighterId, r.orgId, r.id),
|
|
15
|
-
slug: pickString(asRecord(entity)?.slug, r.orgSlug, r.teamSlug, r.slug),
|
|
66
|
+
slug: pickString(asRecord(entity)?.slug, r.orgSlug, r.teamSlug, r.slug, r.fighterSlug),
|
|
16
67
|
name,
|
|
17
68
|
},
|
|
18
69
|
played: r.played ?? r.gamesPlayed ?? r.matches ?? null,
|
|
@@ -24,7 +75,10 @@ function normalizeStandingRow(row, index) {
|
|
|
24
75
|
streak: r.streak ?? null,
|
|
25
76
|
meta: {
|
|
26
77
|
...(r.division ? { division: r.division } : {}),
|
|
78
|
+
...(r.normalizedDivision ? { normalizedDivision: r.normalizedDivision } : {}),
|
|
27
79
|
...(r.region ? { region: r.region } : {}),
|
|
80
|
+
...(isChampion ? { isChampion: true } : {}),
|
|
81
|
+
...(isInterim ? { isInterim: true } : {}),
|
|
28
82
|
},
|
|
29
83
|
};
|
|
30
84
|
}
|
package/package.json
CHANGED