@chessceo/mcp 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/auto.js +291 -0
- package/dist/analysis/deep.js +157 -0
- package/dist/analysis/file_handle.js +156 -0
- package/dist/analysis/response.js +181 -0
- package/dist/courses.js +167 -0
- package/dist/index.js +13 -1501
- package/dist/pgn/exporter.js +13 -2
- package/dist/pgn/parser.js +46 -7
- package/dist/pgn/paths.js +37 -14
- package/dist/pgn/types.js +8 -5
- package/dist/prep/library.js +92 -0
- package/dist/prep/mutations.js +170 -0
- package/dist/prep/read.js +242 -0
- package/dist/response_transforms.js +169 -0
- package/package.json +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Prep-file reads and list-queries. Everything the LLM calls to inspect
|
|
2
|
+
// a file WITHOUT mutating it:
|
|
3
|
+
// - readPrepFile (with four view modes: compact / full / spine / pgn)
|
|
4
|
+
// - listNodes (cheap filter-based node queries)
|
|
5
|
+
// - listTranspositions (positions occurring 2+ times in the file)
|
|
6
|
+
//
|
|
7
|
+
// Extracted from index.ts in v0.44 as part of the file split. Was
|
|
8
|
+
// originally split off the read-case handler because both readPrepFile
|
|
9
|
+
// and listNodes need the same load-parse pipeline and share the
|
|
10
|
+
// compact / spine / pgn view logic.
|
|
11
|
+
import { fetchGame } from "../http.js";
|
|
12
|
+
import { parsePGN } from "../pgn/parser.js";
|
|
13
|
+
import { exportPGN } from "../pgn/exporter.js";
|
|
14
|
+
import { buildFenIndex, buildIdIndex, positionKey, resolveNodeId, ROOT_ID, } from "../pgn/paths.js";
|
|
15
|
+
import { getNodeByPath } from "../analysis/file_handle.js";
|
|
16
|
+
export async function loadPrepFile(id) {
|
|
17
|
+
const g = await fetchGame(id);
|
|
18
|
+
// Echo the composite id back so read_prep_file responses match the
|
|
19
|
+
// exact id the LLM passed in. The backend returns the raw game_id;
|
|
20
|
+
// recompose so the LLM never sees the split form.
|
|
21
|
+
return { file: parsePGN(g.pgnContent), version: g.version, fileIdEcho: id, pgn: g.pgnContent };
|
|
22
|
+
}
|
|
23
|
+
// Recursively project a PrepNode into the requested view. `depthLeft`
|
|
24
|
+
// null → unlimited; 0 → just the node without children.
|
|
25
|
+
//
|
|
26
|
+
// `fenIndex` (optional) enables the `transposes_to` field — for each
|
|
27
|
+
// node whose position also appears elsewhere in the SAME file, we
|
|
28
|
+
// annotate it with the OTHER occurrences' ids. Pass null (the default)
|
|
29
|
+
// to skip the annotation entirely; passing the map costs one lookup
|
|
30
|
+
// per node projected.
|
|
31
|
+
export function projectNode(node, view, depthLeft, fenIndex = null) {
|
|
32
|
+
const base = {
|
|
33
|
+
id: node.id,
|
|
34
|
+
san: node.san,
|
|
35
|
+
ply: node.ply,
|
|
36
|
+
};
|
|
37
|
+
if (node.nags && node.nags.length > 0)
|
|
38
|
+
base.nags = node.nags;
|
|
39
|
+
if (node.comment)
|
|
40
|
+
base.comment = node.comment;
|
|
41
|
+
if (node.ceoEval)
|
|
42
|
+
base.ceoEval = node.ceoEval;
|
|
43
|
+
if (view === "full") {
|
|
44
|
+
base.fen = node.fen;
|
|
45
|
+
if (node.annotations)
|
|
46
|
+
base.annotations = node.annotations;
|
|
47
|
+
}
|
|
48
|
+
if (fenIndex && node.id !== ROOT_ID) {
|
|
49
|
+
const group = fenIndex.get(positionKey(node.fen));
|
|
50
|
+
if (group && group.length > 1) {
|
|
51
|
+
const others = group.filter(n => n.id !== node.id).map(n => n.id);
|
|
52
|
+
if (others.length > 0)
|
|
53
|
+
base.transposes_to = others;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Children handling depends on view + depth budget.
|
|
57
|
+
const showChildren = depthLeft === null || depthLeft > 0;
|
|
58
|
+
const childDepth = depthLeft === null ? null : depthLeft - 1;
|
|
59
|
+
if (showChildren && node.children.length > 0) {
|
|
60
|
+
if (view === "spine") {
|
|
61
|
+
// Only follow children[0] — collapses the tree to the mainline.
|
|
62
|
+
base.children = [projectNode(node.children[0], view, childDepth, fenIndex)];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
base.children = node.children.map(c => projectNode(c, view, childDepth, fenIndex));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
base.children = [];
|
|
70
|
+
}
|
|
71
|
+
return base;
|
|
72
|
+
}
|
|
73
|
+
export async function readPrepFile(args) {
|
|
74
|
+
const id = String(args.id);
|
|
75
|
+
const view = (typeof args.view === "string" && ["compact", "full", "spine", "pgn"].includes(args.view))
|
|
76
|
+
? args.view
|
|
77
|
+
: "compact";
|
|
78
|
+
const startNodeId = typeof args.node_id === "string" && args.node_id.length > 0 ? args.node_id : ROOT_ID;
|
|
79
|
+
const maxDepth = typeof args.max_depth === "number" && args.max_depth >= 0 ? args.max_depth : null;
|
|
80
|
+
const { file, version, fileIdEcho, pgn } = await loadPrepFile(id);
|
|
81
|
+
const idIndex = buildIdIndex(file.root);
|
|
82
|
+
const path = resolveNodeId(idIndex, startNodeId);
|
|
83
|
+
const anchor = getNodeByPath(file.root, path);
|
|
84
|
+
const fenIndex = buildFenIndex(file.root);
|
|
85
|
+
// How many DISTINCT positions in the file appear more than once,
|
|
86
|
+
// and how many nodes are involved. Shown in the header so the LLM
|
|
87
|
+
// sees at a glance whether transpositions matter here before diving
|
|
88
|
+
// into the tree.
|
|
89
|
+
let transGroups = 0;
|
|
90
|
+
let transNodes = 0;
|
|
91
|
+
for (const arr of fenIndex.values()) {
|
|
92
|
+
if (arr.length > 1) {
|
|
93
|
+
transGroups++;
|
|
94
|
+
transNodes += arr.length;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const header = {
|
|
98
|
+
id: fileIdEcho ?? id,
|
|
99
|
+
version,
|
|
100
|
+
tags: file.tags,
|
|
101
|
+
view,
|
|
102
|
+
node_id: startNodeId,
|
|
103
|
+
max_depth: maxDepth,
|
|
104
|
+
transposition_groups: transGroups,
|
|
105
|
+
transposition_nodes: transNodes,
|
|
106
|
+
};
|
|
107
|
+
if (view === "pgn") {
|
|
108
|
+
// For the root, just return the file's actual PGN as-is. For a
|
|
109
|
+
// subtree, build a mini-Game from the anchor and export it. Keeps
|
|
110
|
+
// formatting identical to what the app renders.
|
|
111
|
+
if (startNodeId === ROOT_ID && (maxDepth === null || maxDepth >= 999)) {
|
|
112
|
+
return { ...header, pgn };
|
|
113
|
+
}
|
|
114
|
+
// Truncate to a subtree with max_depth. Simple: walk the anchor's
|
|
115
|
+
// subtree, produce a synthetic PGN starting from the anchor's FEN.
|
|
116
|
+
const subtreePgn = exportSubtreePgn(file, anchor, maxDepth);
|
|
117
|
+
return { ...header, pgn: subtreePgn };
|
|
118
|
+
}
|
|
119
|
+
return { ...header, tree: projectNode(anchor, view, maxDepth, fenIndex) };
|
|
120
|
+
}
|
|
121
|
+
// Produce a PGN string for a subtree rooted at `anchor`, truncated
|
|
122
|
+
// at `maxDepth` plies below (null = unlimited). Reuses the exporter
|
|
123
|
+
// by building a synthetic PrepFile whose root is a shallow clone of
|
|
124
|
+
// the anchor with its children trimmed to depth.
|
|
125
|
+
function exportSubtreePgn(file, anchor, maxDepth) {
|
|
126
|
+
const trim = (n, depthLeft) => {
|
|
127
|
+
if (depthLeft !== null && depthLeft <= 0)
|
|
128
|
+
return { ...n, children: [] };
|
|
129
|
+
const next = depthLeft === null ? null : depthLeft - 1;
|
|
130
|
+
return { ...n, children: n.children.map(c => trim(c, next)) };
|
|
131
|
+
};
|
|
132
|
+
const trimmedAnchor = trim(anchor, maxDepth);
|
|
133
|
+
// If the anchor IS the root, exporter handles it. If it's an inner
|
|
134
|
+
// node, we set the root's FEN to the anchor's position and hang the
|
|
135
|
+
// trimmed subtree off it. Tags carried over.
|
|
136
|
+
if (anchor.id === ROOT_ID) {
|
|
137
|
+
return exportPGN({ tags: file.tags, root: trimmedAnchor });
|
|
138
|
+
}
|
|
139
|
+
const syntheticRoot = {
|
|
140
|
+
id: ROOT_ID,
|
|
141
|
+
san: null,
|
|
142
|
+
fen: anchor.fen,
|
|
143
|
+
ply: 0,
|
|
144
|
+
children: trimmedAnchor.children,
|
|
145
|
+
};
|
|
146
|
+
const tags = { ...file.tags, FEN: anchor.fen, SetUp: "1" };
|
|
147
|
+
return exportPGN({ tags, root: syntheticRoot });
|
|
148
|
+
}
|
|
149
|
+
export async function listNodes(args) {
|
|
150
|
+
const id = String(args.id);
|
|
151
|
+
const filter = String(args.filter || "");
|
|
152
|
+
const startNodeId = typeof args.node_id === "string" && args.node_id.length > 0 ? args.node_id : ROOT_ID;
|
|
153
|
+
const maxDepth = typeof args.max_depth === "number" && args.max_depth >= 0 ? args.max_depth : null;
|
|
154
|
+
const { file } = await loadPrepFile(id);
|
|
155
|
+
const idIndex = buildIdIndex(file.root);
|
|
156
|
+
const path = resolveNodeId(idIndex, startNodeId);
|
|
157
|
+
const anchor = getNodeByPath(file.root, path);
|
|
158
|
+
const fenIndex = filter === "transpositions" ? buildFenIndex(file.root) : null;
|
|
159
|
+
const hits = [];
|
|
160
|
+
const walk = (node, depthLeft, spineOnly) => {
|
|
161
|
+
// Root has no san — never emit it as a match. Everything else is fair game.
|
|
162
|
+
if (node.id !== ROOT_ID) {
|
|
163
|
+
let include = false;
|
|
164
|
+
let extra = {};
|
|
165
|
+
switch (filter) {
|
|
166
|
+
case "missing_eval":
|
|
167
|
+
include = !node.ceoEval;
|
|
168
|
+
break;
|
|
169
|
+
case "has_comment":
|
|
170
|
+
include = !!(node.comment && node.comment.length > 0);
|
|
171
|
+
if (include)
|
|
172
|
+
extra.comment_preview = (node.comment || "").slice(0, 80);
|
|
173
|
+
break;
|
|
174
|
+
case "has_annotations":
|
|
175
|
+
include = !!(node.annotations && (node.annotations.arrows.length > 0 || node.annotations.highlights.length > 0));
|
|
176
|
+
break;
|
|
177
|
+
case "novelties":
|
|
178
|
+
include = !!(node.nags && node.nags.includes("$146"));
|
|
179
|
+
break;
|
|
180
|
+
case "leaves":
|
|
181
|
+
include = node.children.length === 0;
|
|
182
|
+
break;
|
|
183
|
+
case "mainline":
|
|
184
|
+
include = spineOnly;
|
|
185
|
+
break;
|
|
186
|
+
case "transpositions": {
|
|
187
|
+
const group = fenIndex.get(positionKey(node.fen));
|
|
188
|
+
if (group && group.length > 1) {
|
|
189
|
+
include = true;
|
|
190
|
+
extra.transposes_to = group.filter(n => n.id !== node.id).map(n => n.id);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "all":
|
|
195
|
+
include = true;
|
|
196
|
+
break;
|
|
197
|
+
default:
|
|
198
|
+
throw new Error(`unknown filter: ${filter}`);
|
|
199
|
+
}
|
|
200
|
+
if (include) {
|
|
201
|
+
const hit = { node_id: node.id, san: node.san, ply: node.ply };
|
|
202
|
+
Object.assign(hit, extra);
|
|
203
|
+
hits.push(hit);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (depthLeft !== null && depthLeft <= 0)
|
|
207
|
+
return;
|
|
208
|
+
const nextDepth = depthLeft === null ? null : depthLeft - 1;
|
|
209
|
+
if (filter === "mainline" && spineOnly) {
|
|
210
|
+
if (node.children.length > 0)
|
|
211
|
+
walk(node.children[0], nextDepth, true);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
for (const c of node.children)
|
|
215
|
+
walk(c, nextDepth, filter === "mainline");
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const rootIsSpineForFilter = filter === "mainline";
|
|
219
|
+
walk(anchor, maxDepth, rootIsSpineForFilter);
|
|
220
|
+
return { file_id: id, filter, node_id: startNodeId, max_depth: maxDepth, count: hits.length, nodes: hits };
|
|
221
|
+
}
|
|
222
|
+
// list_transpositions — every position that occurs 2+ times in the
|
|
223
|
+
// file, so the LLM knows where its analysis / prose will double up.
|
|
224
|
+
export async function listTranspositions(args) {
|
|
225
|
+
const id = String(args.id);
|
|
226
|
+
const { file } = await loadPrepFile(id);
|
|
227
|
+
const fenIndex = buildFenIndex(file.root);
|
|
228
|
+
const groups = [];
|
|
229
|
+
for (const [key, arr] of fenIndex.entries()) {
|
|
230
|
+
if (arr.length < 2)
|
|
231
|
+
continue;
|
|
232
|
+
groups.push({
|
|
233
|
+
position_key: key,
|
|
234
|
+
size: arr.length,
|
|
235
|
+
node_ids: arr.map(n => n.id),
|
|
236
|
+
sans: arr.map(n => n.san),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
groups.sort((a, b) => b.size - a.size || a.position_key.localeCompare(b.position_key));
|
|
240
|
+
const nodeCount = groups.reduce((s, g) => s + g.size, 0);
|
|
241
|
+
return { file_id: id, group_count: groups.length, node_count: nodeCount, groups };
|
|
242
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Response shape transformations applied to backend payloads before
|
|
2
|
+
// they reach the LLM. Two responsibilities:
|
|
3
|
+
// - Drop internal / debug-only fields the LLM doesn't need
|
|
4
|
+
// (`hash`, `plyNumber`, `relevance`, etc.).
|
|
5
|
+
// - Rewrite backend jargon into LLM-friendly names
|
|
6
|
+
// (`transpositions` → `reachedViaTransposition`, `hotness` →
|
|
7
|
+
// `fashionScore`, UCI moves → SAN).
|
|
8
|
+
//
|
|
9
|
+
// Plus a couple of tool-input adapters that also live here for lack of
|
|
10
|
+
// a better home:
|
|
11
|
+
// - `trimMovesToPly` (used by trimGamesMovetext, exported for reuse).
|
|
12
|
+
// - `normalizeSourceForBackend` (prepare_opponent request adapter).
|
|
13
|
+
//
|
|
14
|
+
// Extracted from index.ts in v0.44 as part of the file split.
|
|
15
|
+
import { uciMoveToSAN } from "./analysis/response.js";
|
|
16
|
+
// Strip cruft the LLM doesn't need from the DB-position response.
|
|
17
|
+
// Called AFTER trimGamesMovetext so plyNumber survives long enough to
|
|
18
|
+
// slice each game's movetext. Also renames the `transpositions` field
|
|
19
|
+
// to something the LLM can parse without knowing chess-DB jargon.
|
|
20
|
+
export function stripPositionResponse(r) {
|
|
21
|
+
if (!r || typeof r !== "object")
|
|
22
|
+
return;
|
|
23
|
+
const t = r;
|
|
24
|
+
delete t.hash; // internal zobrist string
|
|
25
|
+
delete t.source; // internal "database" marker; we overwrite with our own .source
|
|
26
|
+
delete t.totalGames; // duplicates statistics.totalCount often; hasMore covers pagination
|
|
27
|
+
if (Array.isArray(t.moves)) {
|
|
28
|
+
for (const m of t.moves) {
|
|
29
|
+
if (typeof m.transpositions === "number") {
|
|
30
|
+
m.reachedViaTransposition = m.transpositions;
|
|
31
|
+
delete m.transpositions;
|
|
32
|
+
}
|
|
33
|
+
// Backend calls it "hotness" — a 0-100 time-decayed popularity score
|
|
34
|
+
// (recent + played often = high). Rename to something an LLM can read
|
|
35
|
+
// without guessing it means "on a winning streak".
|
|
36
|
+
if (typeof m.hotness === "number") {
|
|
37
|
+
m.fashionScore = m.hotness;
|
|
38
|
+
delete m.hotness;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(t.games)) {
|
|
43
|
+
for (const g of t.games) {
|
|
44
|
+
delete g.gameId;
|
|
45
|
+
delete g.whiteTitle;
|
|
46
|
+
delete g.blackTitle;
|
|
47
|
+
delete g.whiteTeam;
|
|
48
|
+
delete g.blackTeam;
|
|
49
|
+
delete g.round;
|
|
50
|
+
delete g.plyNumber;
|
|
51
|
+
delete g.relevance;
|
|
52
|
+
delete g.site;
|
|
53
|
+
delete g.ply;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Trim every game's `moves` field to just the plies AFTER the queried
|
|
58
|
+
// position, using each game's `plyNumber`. Massive token save — a game
|
|
59
|
+
// 80 plies long queried at ply 12 drops to ~68 plies of movetext. Ports
|
|
60
|
+
// the frontend's GamesTable.getMoveDisplay() trim logic.
|
|
61
|
+
export function trimGamesMovetext(response) {
|
|
62
|
+
if (!response || typeof response !== "object")
|
|
63
|
+
return;
|
|
64
|
+
const r = response;
|
|
65
|
+
if (!Array.isArray(r.games))
|
|
66
|
+
return;
|
|
67
|
+
for (const g of r.games) {
|
|
68
|
+
if (typeof g.moves === "string" && typeof g.plyNumber === "number" && g.plyNumber > 0) {
|
|
69
|
+
g.moves = trimMovesToPly(g.moves, g.plyNumber);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function trimMovesToPly(moves, plyNumber) {
|
|
74
|
+
// Split into plain SAN tokens, dropping standalone move-number tokens
|
|
75
|
+
// ("1.", "12...") and any glued number prefix on a SAN token ("1.e4").
|
|
76
|
+
// Result markers ("*", "1-0", "0-1", "1/2-1/2") are stripped so they
|
|
77
|
+
// don't get counted as plies.
|
|
78
|
+
const tokens = [];
|
|
79
|
+
for (const chunk of moves.split(/\s+/)) {
|
|
80
|
+
if (!chunk)
|
|
81
|
+
continue;
|
|
82
|
+
const cleaned = chunk.replace(/^\d+\.+/, "");
|
|
83
|
+
if (!cleaned)
|
|
84
|
+
continue;
|
|
85
|
+
if (/^(1-0|0-1|1\/2-1\/2|\*)$/.test(cleaned))
|
|
86
|
+
continue;
|
|
87
|
+
tokens.push(cleaned);
|
|
88
|
+
}
|
|
89
|
+
const remaining = tokens.slice(plyNumber);
|
|
90
|
+
if (remaining.length === 0)
|
|
91
|
+
return "";
|
|
92
|
+
// Reconstruct with move numbering. First move gets "N..." if it's
|
|
93
|
+
// Black's move (starting the slice mid-move-pair), so the reader knows
|
|
94
|
+
// moves were dropped.
|
|
95
|
+
const out = [];
|
|
96
|
+
let ply = plyNumber;
|
|
97
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
98
|
+
const san = remaining[i];
|
|
99
|
+
const moveNumber = Math.floor(ply / 2) + 1;
|
|
100
|
+
if (ply % 2 === 0) {
|
|
101
|
+
out.push(`${moveNumber}. ${san}`);
|
|
102
|
+
}
|
|
103
|
+
else if (i === 0) {
|
|
104
|
+
out.push(`${moveNumber}... ${san}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
out.push(san);
|
|
108
|
+
}
|
|
109
|
+
ply++;
|
|
110
|
+
}
|
|
111
|
+
return out.join(" ");
|
|
112
|
+
}
|
|
113
|
+
// Rewrite availableMoves[].move UCI → SAN. The prep + position-stats
|
|
114
|
+
// endpoints return moves in UCI on the wire — same LLM-readability
|
|
115
|
+
// concern as engine PVs, and the same wrapper-only fix. Passes the
|
|
116
|
+
// response through unchanged if there's no availableMoves array.
|
|
117
|
+
export function convertAvailableMovesToSAN(raw, fen) {
|
|
118
|
+
if (!raw || typeof raw !== "object")
|
|
119
|
+
return raw;
|
|
120
|
+
const r = raw;
|
|
121
|
+
if (!Array.isArray(r.availableMoves))
|
|
122
|
+
return raw;
|
|
123
|
+
for (const m of r.availableMoves) {
|
|
124
|
+
if (typeof m.move === "string" && m.move.length >= 4) {
|
|
125
|
+
m.move = uciMoveToSAN(fen, m.move);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return raw;
|
|
129
|
+
}
|
|
130
|
+
// Normalize one MCP `prepare_opponent` source into the shape the backend's
|
|
131
|
+
// /api/chess/prep/prepare-multi expects. Handles two impedance mismatches:
|
|
132
|
+
// - snake_case → camelCase (fide_id → fideId, start_month → startMonth, etc.)
|
|
133
|
+
// - the unified `time_control` string → per-source-type filter:
|
|
134
|
+
// * fide / chesscom → timeFormats: ["Classical" | "Rapid" | "Blitz"]
|
|
135
|
+
// * lichess → perfType: "classical" | "rapid" | "blitz" | "bullet"
|
|
136
|
+
// Backend validates required fields per source type, so we don't need to
|
|
137
|
+
// pre-reject missing username/fideId here — it'll come back as a 400 the
|
|
138
|
+
// LLM can act on.
|
|
139
|
+
export function normalizeSourceForBackend(src, idx) {
|
|
140
|
+
const type = typeof src.type === "string" ? src.type : "";
|
|
141
|
+
if (type !== "fide" && type !== "chesscom" && type !== "lichess") {
|
|
142
|
+
throw new Error(`sources[${idx}].type must be one of fide|chesscom|lichess (got ${JSON.stringify(src.type)})`);
|
|
143
|
+
}
|
|
144
|
+
const out = { type };
|
|
145
|
+
if (typeof src.fide_id === "number")
|
|
146
|
+
out.fideId = src.fide_id;
|
|
147
|
+
if (typeof src.username === "string" && src.username.trim() !== "")
|
|
148
|
+
out.username = src.username.trim();
|
|
149
|
+
if (typeof src.color === "string" && (src.color === "white" || src.color === "black"))
|
|
150
|
+
out.color = src.color;
|
|
151
|
+
if (typeof src.start_month === "string" && src.start_month.trim() !== "")
|
|
152
|
+
out.startMonth = src.start_month.trim();
|
|
153
|
+
if (typeof src.end_month === "string" && src.end_month.trim() !== "")
|
|
154
|
+
out.endMonth = src.end_month.trim();
|
|
155
|
+
if (typeof src.exclude_online === "boolean")
|
|
156
|
+
out.excludeOnline = src.exclude_online;
|
|
157
|
+
const tc = typeof src.time_control === "string" ? src.time_control : "";
|
|
158
|
+
if (tc) {
|
|
159
|
+
if (type === "lichess") {
|
|
160
|
+
out.perfType = tc;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// fide + chesscom take a titlecased timeFormats array.
|
|
164
|
+
const titled = tc.charAt(0).toUpperCase() + tc.slice(1);
|
|
165
|
+
out.timeFormats = [titled];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
package/package.json
CHANGED