chesspal 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +11 -12
  2. package/dist/cli/cli.js +83 -3
  3. package/dist/cli/command-line.js +26 -9
  4. package/dist/cli/icon.d.ts +25 -0
  5. package/dist/cli/icon.js +80 -0
  6. package/dist/commands/agents.d.ts +19 -2
  7. package/dist/commands/agents.js +41 -14
  8. package/dist/commands/ai-say.d.ts +2 -1
  9. package/dist/commands/ai-say.js +3 -4
  10. package/dist/commands/analyse-game.d.ts +6 -0
  11. package/dist/commands/analyse-game.js +51 -15
  12. package/dist/commands/annotate-game.d.ts +4 -0
  13. package/dist/commands/annotate-game.js +60 -20
  14. package/dist/commands/answer-lines.d.ts +10 -4
  15. package/dist/commands/answer-lines.js +30 -4
  16. package/dist/commands/coach.js +6 -4
  17. package/dist/commands/context.d.ts +11 -1
  18. package/dist/commands/context.js +5 -0
  19. package/dist/commands/delete-review.js +9 -4
  20. package/dist/commands/game-summary.d.ts +9 -2
  21. package/dist/commands/game-summary.js +11 -0
  22. package/dist/commands/index.d.ts +4 -3
  23. package/dist/commands/index.js +8 -5
  24. package/dist/commands/init.js +2 -4
  25. package/dist/commands/instructions.d.ts +9 -0
  26. package/dist/commands/instructions.js +30 -0
  27. package/dist/commands/lead-input.d.ts +1 -1
  28. package/dist/commands/lead-input.js +7 -4
  29. package/dist/commands/look.d.ts +9 -5
  30. package/dist/commands/look.js +19 -9
  31. package/dist/commands/page-link.js +6 -5
  32. package/dist/commands/past-games.d.ts +1 -1
  33. package/dist/commands/past-games.js +12 -5
  34. package/dist/commands/player-move.js +2 -3
  35. package/dist/commands/ponder.js +1 -1
  36. package/dist/commands/position.js +2 -2
  37. package/dist/commands/profiles.d.ts +23 -9
  38. package/dist/commands/profiles.js +76 -35
  39. package/dist/commands/ratings.d.ts +8 -0
  40. package/dist/commands/ratings.js +45 -0
  41. package/dist/commands/registry.d.ts +1 -1
  42. package/dist/commands/registry.js +3 -2
  43. package/dist/commands/say.d.ts +11 -6
  44. package/dist/commands/say.js +21 -13
  45. package/dist/commands/status.d.ts +6 -2
  46. package/dist/commands/status.js +70 -26
  47. package/dist/commands/study.d.ts +11 -1
  48. package/dist/commands/study.js +39 -7
  49. package/dist/commands/view.d.ts +17 -5
  50. package/dist/commands/view.js +67 -10
  51. package/dist/core/facts.d.ts +14 -2
  52. package/dist/core/facts.js +32 -7
  53. package/dist/core/game.d.ts +5 -0
  54. package/dist/core/game.js +9 -0
  55. package/dist/core/index.d.ts +2 -1
  56. package/dist/core/index.js +1 -1
  57. package/dist/core/placement.d.ts +63 -0
  58. package/dist/core/placement.js +259 -0
  59. package/dist/core/possibilities.d.ts +38 -0
  60. package/dist/core/possibilities.js +254 -0
  61. package/dist/core/review.d.ts +6 -1
  62. package/dist/core/review.js +21 -6
  63. package/dist/games/ai-model.d.ts +4 -2
  64. package/dist/games/ai-model.js +10 -1
  65. package/dist/games/analysis.d.ts +31 -4
  66. package/dist/games/analysis.js +44 -5
  67. package/dist/games/archive.d.ts +6 -3
  68. package/dist/games/archive.js +13 -5
  69. package/dist/games/current-game.d.ts +26 -2
  70. package/dist/games/current-game.js +57 -21
  71. package/dist/games/files.d.ts +7 -0
  72. package/dist/games/files.js +48 -1
  73. package/dist/games/index.d.ts +4 -4
  74. package/dist/games/index.js +4 -4
  75. package/dist/games/table-talk.d.ts +2 -0
  76. package/dist/games/table-talk.js +5 -1
  77. package/dist/games/talk.d.ts +4 -3
  78. package/dist/games/talk.js +3 -3
  79. package/dist/home/data-folder.d.ts +2 -0
  80. package/dist/home/data-folder.js +5 -1
  81. package/dist/home/index.d.ts +1 -1
  82. package/dist/home/index.js +1 -1
  83. package/dist/host-files/agents.d.ts +2 -2
  84. package/dist/host-files/agents.js +12 -6
  85. package/dist/host-files/host-files.d.ts +5 -3
  86. package/dist/host-files/host-files.js +15 -8
  87. package/dist/host-files/index.d.ts +2 -2
  88. package/dist/host-files/index.js +2 -2
  89. package/dist/page/app/app.js +59 -22
  90. package/dist/page/app/keys.js +16 -2
  91. package/dist/page/app/served.js +2 -2
  92. package/dist/page/app/title.js +17 -0
  93. package/dist/page/avatars/torretta.js +4 -4
  94. package/dist/page/board/board.js +2 -1
  95. package/dist/page/board/marks.js +4 -24
  96. package/dist/page/board/signs.js +23 -0
  97. package/dist/page/dialogs/analyses.js +4 -4
  98. package/dist/page/dialogs/dialog.js +4 -1
  99. package/dist/page/dialogs/dialogs.css +84 -10
  100. package/dist/page/dialogs/profiles.js +95 -42
  101. package/dist/page/game/column.js +6 -2
  102. package/dist/page/game/game.css +12 -0
  103. package/dist/page/game/move-list.js +2 -2
  104. package/dist/page/game/plies.js +11 -0
  105. package/dist/page/guide/blocks.js +67 -0
  106. package/dist/page/guide/en.md +66 -0
  107. package/dist/page/guide/foot.js +18 -0
  108. package/dist/page/guide/guide.css +200 -0
  109. package/dist/page/guide/guide.js +89 -0
  110. package/dist/page/guide/it.md +66 -0
  111. package/dist/page/guide/markdown.js +67 -0
  112. package/dist/page/helps/helps.css +10 -11
  113. package/dist/page/helps/helps.js +15 -15
  114. package/dist/page/layout/favicon.svg +13 -0
  115. package/dist/page/layout/header.js +3 -3
  116. package/dist/page/layout/icon.icns +0 -0
  117. package/dist/page/layout/icon.ico +0 -0
  118. package/dist/page/layout/icon.png +0 -0
  119. package/dist/page/layout/layout.css +140 -9
  120. package/dist/page/layout/window.js +45 -0
  121. package/dist/page/legend/legend.css +203 -0
  122. package/dist/page/legend/legend.js +169 -0
  123. package/dist/page/legend/view.js +54 -0
  124. package/dist/page/main.js +4 -1
  125. package/dist/page/players/card.js +1 -1
  126. package/dist/page/profile/profile.js +14 -9
  127. package/dist/page/profile/settings.js +9 -0
  128. package/dist/page/style.css +4 -0
  129. package/dist/page/table/opener.css +65 -0
  130. package/dist/page/table/opener.js +59 -0
  131. package/dist/page/talk/chat.css +97 -0
  132. package/dist/page/talk/talk.css +38 -26
  133. package/dist/page/talk/talk.js +31 -7
  134. package/dist/page/text.js +120 -18
  135. package/dist/page/theme.css +7 -0
  136. package/dist/profiles/index.d.ts +2 -1
  137. package/dist/profiles/index.js +2 -1
  138. package/dist/profiles/profiles.d.ts +10 -15
  139. package/dist/profiles/profiles.js +30 -15
  140. package/dist/profiles/ratings.d.ts +35 -0
  141. package/dist/profiles/ratings.js +61 -0
  142. package/dist/server/bell.d.ts +24 -3
  143. package/dist/server/bell.js +84 -10
  144. package/dist/server/caller.d.ts +2 -1
  145. package/dist/server/caller.js +3 -2
  146. package/dist/server/mcp.d.ts +7 -3
  147. package/dist/server/mcp.js +106 -56
  148. package/dist/server/page/analyses.d.ts +2 -0
  149. package/dist/server/page/analyses.js +20 -5
  150. package/dist/server/page/attach.d.ts +4 -0
  151. package/dist/server/page/attach.js +112 -0
  152. package/dist/server/page/chat.d.ts +60 -0
  153. package/dist/server/page/chat.js +79 -0
  154. package/dist/server/page/events.d.ts +7 -0
  155. package/dist/server/page/events.js +20 -0
  156. package/dist/server/page/files.js +10 -1
  157. package/dist/server/page/game.js +62 -29
  158. package/dist/server/page/page.d.ts +47 -7
  159. package/dist/server/page/page.js +74 -51
  160. package/dist/server/page/profiles.d.ts +5 -0
  161. package/dist/server/page/profiles.js +26 -10
  162. package/dist/server/page/table.d.ts +39 -0
  163. package/dist/server/page/table.js +102 -0
  164. package/dist/server/table/browser.d.ts +6 -0
  165. package/dist/server/table/browser.js +48 -0
  166. package/dist/server/table/hosts/chats.d.ts +7 -0
  167. package/dist/server/table/hosts/chats.js +31 -0
  168. package/dist/server/table/hosts/find.d.ts +2 -0
  169. package/dist/server/table/hosts/find.js +20 -0
  170. package/dist/server/table/hosts/hosts.d.ts +34 -0
  171. package/dist/server/table/hosts/hosts.js +49 -0
  172. package/dist/server/table/hosts/index.d.ts +3 -0
  173. package/dist/server/table/hosts/index.js +4 -0
  174. package/dist/server/table/hosts/play.d.ts +25 -0
  175. package/dist/server/table/hosts/play.js +152 -0
  176. package/dist/server/table/hosts/seat.d.ts +25 -0
  177. package/dist/server/table/hosts/seat.js +74 -0
  178. package/dist/server/table/hosts/terminal.d.ts +6 -0
  179. package/dist/server/table/hosts/terminal.js +63 -0
  180. package/dist/server/table/icon/index.d.ts +32 -0
  181. package/dist/server/table/icon/index.js +83 -0
  182. package/dist/server/table/icon/linux.d.ts +5 -0
  183. package/dist/server/table/icon/linux.js +47 -0
  184. package/dist/server/table/icon/macos.d.ts +3 -0
  185. package/dist/server/table/icon/macos.js +38 -0
  186. package/dist/server/table/icon/windows.d.ts +3 -0
  187. package/dist/server/table/icon/windows.js +48 -0
  188. package/dist/server/table/index.d.ts +6 -0
  189. package/dist/server/table/index.js +7 -0
  190. package/dist/server/table/launch.d.ts +19 -0
  191. package/dist/server/table/launch.js +78 -0
  192. package/dist/server/table/link.d.ts +10 -0
  193. package/dist/server/table/link.js +230 -0
  194. package/dist/server/table/table.d.ts +35 -0
  195. package/dist/server/table/table.js +94 -0
  196. package/dist/sites/chesscom.d.ts +4 -0
  197. package/dist/sites/chesscom.js +25 -0
  198. package/dist/sites/choice.d.ts +17 -0
  199. package/dist/sites/choice.js +13 -0
  200. package/dist/sites/index.d.ts +2 -0
  201. package/dist/sites/index.js +4 -0
  202. package/dist/sites/lichess.d.ts +4 -0
  203. package/dist/sites/lichess.js +32 -0
  204. package/dist/sites/request.d.ts +11 -0
  205. package/dist/sites/request.js +15 -0
  206. package/dist/sites/sites.d.ts +6 -0
  207. package/dist/sites/sites.js +33 -0
  208. package/instructions/analysis.md +1 -1
  209. package/instructions/analyst-agent.md +1 -1
  210. package/instructions/coach.md +2 -1
  211. package/instructions/opponent.md +6 -6
  212. package/instructions/ponder.md +1 -0
  213. package/instructions/start.md +1 -0
  214. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { deleteReview } from "./delete-review.js";
9
9
  import { endGame } from "./end-game.js";
10
10
  import { exportGame } from "./export-game.js";
11
11
  import { init } from "./init.js";
12
+ import { instructions } from "./instructions.js";
12
13
  import { look } from "./look.js";
13
14
  import { newGame } from "./new-game.js";
14
15
  import { pastGames } from "./past-games.js";
@@ -16,14 +17,14 @@ import { playerMove } from "./player-move.js";
16
17
  import { playerSay } from "./player-say.js";
17
18
  import { ponder } from "./ponder.js";
18
19
  import { position } from "./position.js";
19
- import { newProfile, profiles, setAccounts, setAvatar, useProfile } from "./profiles.js";
20
+ import { editProfile, newProfile, profiles, setAvatar, useProfile } from "./profiles.js";
20
21
  import { status } from "./status.js";
21
22
  import { studyGames } from "./study-games.js";
22
23
  // What the server and the command line use besides the list: the context, the reading of an input, and the operations the page shares with the tools.
23
24
  export { aiMove } from "./ai-move.js";
24
25
  export { analyseGame } from "./analyse-game.js";
25
26
  export { gameAt } from "./clock.js";
26
- export { defaultContext } from "./context.js";
27
+ export { defaultContext, speaksItalian } from "./context.js";
27
28
  export { deleteGame } from "./delete-game.js";
28
29
  export { deleteReview } from "./delete-review.js";
29
30
  export { answerDraw, DrawRefused, offerDraw } from "./draw-offer.js";
@@ -31,13 +32,15 @@ export { endGame } from "./end-game.js";
31
32
  export { ChatNotClear, newGame, startNewGame } from "./new-game.js";
32
33
  export { bookOpening, bookPlies } from "./opening-book.js";
33
34
  export { playMove, PositionChanged, positionIn, samePosition } from "./play-move.js";
34
- export { chooseDrawnAvatar, GameGoingOn, newProfile, setAccounts, useProfile } from "./profiles.js";
35
+ export { chooseDrawnAvatar, editProfile, GameGoingOn, newProfile, useProfile } from "./profiles.js";
36
+ export { ratingsAge, refreshRatings } from "./ratings.js";
35
37
  export { baseType, commandLineFieldProblem, inputProblems, isBoolean, isList, takesNumbers } from "./registry.js";
36
38
  export { say } from "./say.js";
37
- export { gameToStudy, namedGame } from "./study.js";
39
+ export { gameToStudy, namedGame, shownPosition } from "./study.js";
38
40
  /** Every command of Chesspal, in the order the command line lists them and the MCP server offers them. */
39
41
  export const commands = [
40
42
  init,
43
+ instructions,
41
44
  status,
42
45
  newGame,
43
46
  playerMove,
@@ -60,5 +63,5 @@ export const commands = [
60
63
  newProfile,
61
64
  useProfile,
62
65
  setAvatar,
63
- setAccounts,
66
+ editProfile,
64
67
  ];
@@ -4,6 +4,7 @@ import { relative, resolve } from "node:path";
4
4
  import { styleText } from "node:util";
5
5
  import { z } from "zod";
6
6
  import { codexHooksPath, setUpHosts } from "../host-files/index.js";
7
+ import { speaksItalian } from "./context.js";
7
8
  import { defineCommand } from "./registry.js";
8
9
  /** Makes a folder the player's chess table: the files through which Claude Code and Codex find Chesspal and the opponent's instructions (D-31). */
9
10
  export const init = defineCommand({
@@ -28,7 +29,7 @@ export const init = defineCommand({
28
29
  };
29
30
  },
30
31
  format: ({ folder, written, enter, hooksChanged }, context) => {
31
- const words = wordsFor(context?.language);
32
+ const words = speaksItalian(context?.language) ? italian : english;
32
33
  const steps = [words.open(enter === undefined ? undefined : quoted(enter)), ...words.steps];
33
34
  return [
34
35
  styleText(["bold", "green"], `♜ ${written.length === 0 ? words.ready(folder) : words.set(folder)}`),
@@ -64,9 +65,6 @@ const italian = {
64
65
  hooksChanged: "Gli «hook» di Chesspal per Codex sono cambiati: quando Codex te lo chiede, scegli di nuovo Trust all.",
65
66
  updated: (count) => count === 1 ? "Aggiornato 1 file." : `Aggiornati ${count} file.`,
66
67
  };
67
- function wordsFor(language) {
68
- return language?.slice(0, 2).toLowerCase() === "it" ? italian : english;
69
- }
70
68
  /** A path as a shell reads it as one word: in quotes when it has spaces. */
71
69
  function quoted(path) {
72
70
  return /\s/.test(path) ? `"${path}"` : path;
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ import type { Context } from "./context.ts";
3
+ /** How to play as the player's opponent, which the chat reads before its first reply and again after a summary (D-102). */
4
+ export declare const instructions: import("./registry.ts").Command<z.ZodObject<{}, z.core.$strip>, string>;
5
+ /**
6
+ * Whether the chat has read the opponent's instructions since it started, or was cleared or summarized, as its start keys them (D-57); without the host's
7
+ * note of it, since the MCP server started. Always on the command line, which keeps nothing.
8
+ */
9
+ export declare function instructionsRead(context: Context, chatStart: string | undefined): boolean;
@@ -0,0 +1,30 @@
1
+ // The opponent's instructions, which the MCP server gives so that they follow the version installed (D-102).
2
+ import { z } from "zod";
3
+ import { lastChatStart } from "../games/index.js";
4
+ import { opponentInstructions } from "../host-files/index.js";
5
+ import { defineCommand } from "./registry.js";
6
+ /** How to play as the player's opponent, which the chat reads before its first reply and again after a summary (D-102). */
7
+ export const instructions = defineCommand({
8
+ name: "instructions",
9
+ description: "How to play as the player's opponent at this table: the tools, the board page, how to talk. Call it before your first reply in every chat, "
10
+ + "and again after the conversation is summarized; then call `status`.",
11
+ input: z.object({}),
12
+ readOnly: true,
13
+ idempotent: true,
14
+ alwaysLoad: true,
15
+ run: (input, context) => {
16
+ context.told?.add(readKey(lastChatStart(context.folder)?.at));
17
+ return opponentInstructions();
18
+ },
19
+ format: (text) => text,
20
+ });
21
+ /**
22
+ * Whether the chat has read the opponent's instructions since it started, or was cleared or summarized, as its start keys them (D-57); without the host's
23
+ * note of it, since the MCP server started. Always on the command line, which keeps nothing.
24
+ */
25
+ export function instructionsRead(context, chatStart) {
26
+ return context.told === undefined || context.told.has(readKey(chatStart));
27
+ }
28
+ function readKey(chatStart) {
29
+ return `instructions ${chatStart ?? ""}`;
30
+ }
@@ -4,5 +4,5 @@ import { type Lead } from "../core/index.ts";
4
4
  export declare const leadInput: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
5
5
  /** Who is ahead in words, the side and by how much, so that the AI reads its own sign back (Anatolij). */
6
6
  export declare function aheadWords(lead: Lead): string;
7
- /** The lead an input gave, checked by leadInput. */
7
+ /** The lead an input gave, refused with how to write it when it is none. */
8
8
  export declare function leadFrom(value: number | string): Lead;
@@ -2,8 +2,7 @@
2
2
  import { z } from "zod";
3
3
  import { readLead } from "../core/index.js";
4
4
  /** A lead as its input reads it: pawns from White's side as a number, +1.5 or -3, or a mate it sees, "#3" White's or "#-3" Black's. */
5
- export const leadInput = z.union([z.number(), z.string()]).refine((value) => readLead(String(value)) !== undefined, 'who is ahead is a number of pawns from White\'s side, 1.5 or -3, or a mate you see in moves of the side that mates, "#3" for White\'s or "#-3" for Black\'s, '
6
- + "up to 99");
5
+ export const leadInput = z.union([z.number(), z.string()]).refine((value) => readLead(String(value)) !== undefined, leadRule());
7
6
  /** Who is ahead in words, the side and by how much, so that the AI reads its own sign back (Anatolij). */
8
7
  export function aheadWords(lead) {
9
8
  if ("mate" in lead) {
@@ -11,11 +10,15 @@ export function aheadWords(lead) {
11
10
  }
12
11
  return lead.pawns === 0 ? "even" : `${lead.pawns > 0 ? "White" : "Black"} ahead by ${Math.abs(lead.pawns)}`;
13
12
  }
14
- /** The lead an input gave, checked by leadInput. */
13
+ /** The lead an input gave, refused with how to write it when it is none. */
15
14
  export function leadFrom(value) {
16
15
  const lead = readLead(String(value));
17
16
  if (lead === undefined) {
18
- throw new Error(`${value} is not who is ahead: give a number of pawns from White's side, 1.5 or -3, or a mate you see, "#3" or "#-3". Nothing changed.`);
17
+ throw new Error(`${JSON.stringify(value)} is no figure: ${leadRule()}. Nothing changed.`);
19
18
  }
20
19
  return lead;
21
20
  }
21
+ function leadRule() {
22
+ return 'who is ahead is a number of pawns from White\'s side, 1.5 or -3, or a mate you see in moves of the side that mates, "#3" for White\'s or "#-3" for '
23
+ + "Black's, up to 99";
24
+ }
@@ -1,16 +1,20 @@
1
1
  import { z } from "zod";
2
- import { type Facts, type Side } from "../core/index.ts";
2
+ import { type Chances, type Facts, type Possibilities, type Side } from "../core/index.ts";
3
3
  import { type GameSummary } from "./game-summary.ts";
4
- /** The game with the facts of its position, whoever is to move. */
4
+ import { type Written } from "./say.ts";
5
+ /** The game with the facts, the possibilities and the threats of its position, whoever is to move. */
5
6
  type Viewed = GameSummary & {
6
7
  facts: Facts;
8
+ possibilities: Possibilities;
7
9
  };
8
- /** A position of a game studied (D-68): where it stands, and its facts. */
10
+ /** A position of a game studied (D-68): where it stands, its facts, its possibilities and the threats. */
9
11
  interface StudyView {
10
12
  where: string;
11
13
  turn: Side;
12
14
  fen: string;
13
15
  facts: Facts;
16
+ possibilities: Possibilities;
17
+ threats: Chances | undefined;
14
18
  }
15
19
  /** The whole view (D-26), when the compact one after the player's move is not enough; or the board for the player, where the host does not show it (D-36). */
16
20
  export declare const look: import("./registry.ts").Command<z.ZodObject<{
@@ -23,9 +27,9 @@ export declare const look: import("./registry.ts").Command<z.ZodObject<{
23
27
  board: string;
24
28
  } | {
25
29
  game: Viewed;
26
- words: string[];
30
+ words: Written[];
27
31
  } | {
28
32
  study: StudyView;
29
- words: string[];
33
+ words: Written[];
30
34
  }>;
31
35
  export {};
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { boardText, materialOf } from "../core/index.js";
2
+ import { boardText, materialOf, sees } from "../core/index.js";
3
3
  import { analystAgent } from "../host-files/index.js";
4
4
  import { gameFolder } from "../profiles/index.js";
5
5
  import { headLines, wordLines } from "./answer-lines.js";
@@ -8,7 +8,7 @@ import { summaryOf } from "./game-summary.js";
8
8
  import { refuseUnseen } from "./levels.js";
9
9
  import { defineCommand } from "./registry.js";
10
10
  import { newWords } from "./say.js";
11
- import { gameToStudy, positionAfter, positionLine, studyInput } from "./study.js";
11
+ import { gameShown, positionAfter, positionLine, studyInput } from "./study.js";
12
12
  import { fullView, materialText } from "./view.js";
13
13
  /** The whole view (D-26), when the compact one after the player's move is not enough; or the board for the player, where the host does not show it (D-36). */
14
14
  export const look = defineCommand({
@@ -34,31 +34,41 @@ export const look = defineCommand({
34
34
  if (only !== "board") {
35
35
  refuseUnseen(current, "tactics", 'look without only: "board"');
36
36
  }
37
- return only === "board" ? { board: boardText(current.game.position.fen) } : { game: { ...summaryOf(current, context.now()), facts: current.game.facts }, words: newWords(context) };
37
+ if (only === "board") {
38
+ return { board: boardText(current.game.position.fen) };
39
+ }
40
+ const { threats } = current.game;
41
+ const viewed = { ...summaryOf(current, context.now()), facts: current.game.facts, possibilities: current.game.possibilities };
42
+ return { game: threats === undefined ? viewed : { ...viewed, threats }, words: newWords(context) };
38
43
  },
39
44
  format: (look) => {
40
45
  if ("board" in look) {
41
46
  return look.board;
42
47
  }
43
- return [...("game" in look ? [...headLines(look.game), ...fullView(look.game.facts, look.game.turn)] : studyLines(look.study)), ...wordLines(look.words)].join("\n");
48
+ return [...("game" in look ? gameLines(look.game) : studyLines(look.study)), ...wordLines(look.words)].join("\n");
44
49
  },
45
50
  });
46
51
  /** A position of a game over, as the view of the game going on gives it. */
47
52
  function studyLook(context, only, name, move) {
48
- const studied = gameToStudy(context, name);
53
+ const studied = gameShown(context, name);
49
54
  const at = positionAfter(studied.record, move);
50
55
  const { position } = at.game;
51
56
  if (only === "board") {
52
57
  return { board: boardText(position.fen) };
53
58
  }
54
- return { study: { where: positionLine(at, studied.name), turn: position.turn, fen: position.fen, facts: at.game.facts }, words: newWords(context) };
59
+ const view = { where: positionLine(at, studied.name), turn: position.turn, fen: position.fen, facts: at.game.facts, possibilities: at.game.possibilities, threats: at.game.threats };
60
+ return { study: view, words: newWords(context) };
61
+ }
62
+ /** The game going on: its head and the whole view, with the other side's chances from the strong level (D-100). */
63
+ function gameLines(game) {
64
+ return [...headLines(game), ...fullView(game.facts, game.turn, { possibilities: game.possibilities, threats: game.threats, strong: sees(game.level, "strategy") })];
55
65
  }
56
- /** Where the position stands, the material, the FEN, and the whole view. */
57
- function studyLines({ where, turn, fen, facts }) {
66
+ /** Where the position stands, the material, the FEN, and the whole view, with every possibility, as the analyst sees it. */
67
+ function studyLines({ where, turn, fen, facts, possibilities, threats }) {
58
68
  return [
59
69
  where,
60
70
  `Material: ${materialText(materialOf(fen))}`,
61
71
  `FEN: ${fen}`,
62
- ...fullView(facts, turn),
72
+ ...fullView(facts, turn, { possibilities, threats, strong: true }),
63
73
  ];
64
74
  }
@@ -5,15 +5,15 @@ export function closedPage(context) {
5
5
  /** The board page's address once a game, for a move written in the chat while no browser has the page open: the game is played there (D-85). */
6
6
  export function pageInvite(context, started) {
7
7
  const page = closedPage(context);
8
- if (page === undefined || context.told === undefined || context.told.has(inviteKey(started))) {
8
+ if (page === undefined || context.told === undefined || context.told.has(inviteKey(context, started))) {
9
9
  return undefined;
10
10
  }
11
- context.told.add(inviteKey(started));
11
+ context.told.add(inviteKey(context, started));
12
12
  return page;
13
13
  }
14
14
  /** Notes the board page's address given for a game, by status or new_game: a move in the chat does not give it again (Priya, round of D-85). */
15
15
  export function pageGiven(context, started) {
16
- context.told?.add(inviteKey(started));
16
+ context.told?.add(inviteKey(context, started));
17
17
  }
18
18
  /** The line that has the AI invite the player to the board page from the chat. */
19
19
  export function inviteLine(url) {
@@ -23,6 +23,7 @@ export function inviteLine(url) {
23
23
  export function pageLine(url, closed) {
24
24
  return closed ? `Board page: ${url}, open in no browser: give the player this address before anything else.` : `Board page: ${url}`;
25
25
  }
26
- function inviteKey(started) {
27
- return `page invite ${started ?? ""}`;
26
+ // Once a game and an address: a page that moved, to a table or from one, is given again (Priya).
27
+ function inviteKey(context, started) {
28
+ return `page invite ${started ?? ""} ${context.page ?? ""}`;
28
29
  }
@@ -7,5 +7,5 @@ export declare const pastGames: import("./registry.ts").Command<z.ZodObject<{
7
7
  } | {
8
8
  pgn: string;
9
9
  }>;
10
- /** The latest games of the archive, one line each as the AI reads them: none when there is no archive. */
10
+ /** The latest games of the archive, one line each as the AI reads them: none when there is no archive. A game closed before any move is no game played. */
11
11
  export declare function pastGameLines(folder: string, count: number): string[];
@@ -19,17 +19,24 @@ export const pastGames = defineCommand({
19
19
  },
20
20
  format: (past) => ("pgn" in past ? past.pgn : past.games.length === 0 ? "No game played yet." : past.games.join("\n")),
21
21
  });
22
- /** The latest games of the archive, one line each as the AI reads them: none when there is no archive. */
22
+ /** The latest games of the archive, one line each as the AI reads them: none when there is no archive. A game closed before any move is no game played. */
23
23
  export function pastGameLines(folder, count) {
24
- return archivedGames(folder).slice(0, count).flatMap((name) => {
24
+ const lines = [];
25
+ for (const name of archivedGames(folder)) {
26
+ if (lines.length === count) {
27
+ break;
28
+ }
25
29
  try {
26
- return [pastGameLine(name, archivedGame(folder, name))];
30
+ const record = archivedGame(folder, name);
31
+ if (record.moves.length > 0) {
32
+ lines.push(pastGameLine(name, record));
33
+ }
27
34
  }
28
35
  catch {
29
36
  // A game the archive cannot read stays out of the memory.
30
- return [];
31
37
  }
32
- });
38
+ }
39
+ return lines;
33
40
  }
34
41
  const listed = 20;
35
42
  /** 2026-11-03 12:04, you (AI) played Black and won 0-1 (White resigns), B50 Sicilian Defense, 15 moves. games/2026-11-03-120423.pgn */
@@ -1,8 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { loadGame } from "../games/index.js";
3
- import { coachAgent } from "../host-files/index.js";
4
3
  import { gameFolder } from "../profiles/index.js";
5
- import { agentsLine, coachOnEnd, coachRuns, withAnalyst } from "./agents.js";
4
+ import { coachOnEnd, coachOnEndLine, coachRuns, withAnalyst } from "./agents.js";
6
5
  import { closingLines, lastMoveText, moveLines, shownMove } from "./answer-lines.js";
7
6
  import {} from "./game-summary.js";
8
7
  import { inviteLine, pageInvite } from "./page-link.js";
@@ -40,7 +39,7 @@ export const playerMove = defineCommand({
40
39
  `Player: ${result.game.lastMove === undefined ? shownMove(result.move) : lastMoveText(result.game.lastMove)}.`,
41
40
  ...moveLines(result),
42
41
  ...(result.invite === undefined ? [] : [inviteLine(result.invite)]),
43
- ...(result.coach === true ? [agentsLine([coachAgent])] : []),
42
+ ...(result.coach === true ? [coachOnEndLine] : []),
44
43
  ...closingLines(result.game, result.words),
45
44
  ].join("\n"),
46
45
  });
@@ -156,7 +156,7 @@ function startOf(current) {
156
156
  const line = [
157
157
  `Position ${game.record.moves.length}: the player (${colourName(player)}) to move, move ${game.position.moveNumber}; you play ${colourName(ai)} ${levelText(level)}.`,
158
158
  `FEN: ${game.position.fen}`,
159
- ...(sees(level, "tactics") ? compactView : boardView)(game.facts, player),
159
+ ...(sees(level, "tactics") ? compactView(game.facts, player, { possibilities: game.possibilities, threats: game.threats, strong: sees(level, "strategy") }) : boardView(game.facts, player)),
160
160
  ].join("\n");
161
161
  const said = talk.slice(-3).map((spoken) => `${spoken.who === "ai" ? "You" : "Player"}: «${spoken.text.replace(/\s+/g, " ")}»`);
162
162
  return {
@@ -5,7 +5,7 @@ import { gameFolder } from "../profiles/index.js";
5
5
  import { gameAt } from "./clock.js";
6
6
  import { refuseUnseen } from "./levels.js";
7
7
  import { defineCommand } from "./registry.js";
8
- import { gameToStudy, positionAfter, positionLine, studyInput } from "./study.js";
8
+ import { gameShown, positionAfter, positionLine, studyInput } from "./study.js";
9
9
  import { colourName, materialText } from "./view.js";
10
10
  /** The positional profile of the game in progress (D-19), or of any position of a game over (D-68): separate facts, never a score. */
11
11
  export const position = defineCommand({
@@ -21,7 +21,7 @@ export const position = defineCommand({
21
21
  sharedWith: [analystAgent],
22
22
  run: ({ game, move }, context) => {
23
23
  if (game !== undefined || move !== undefined) {
24
- const studied = gameToStudy(context, game);
24
+ const studied = gameShown(context, game);
25
25
  const at = positionAfter(studied.record, move);
26
26
  return { ...at.game.profile, where: positionLine(at, studied.name) };
27
27
  }
@@ -12,7 +12,8 @@ export declare const profiles: import("./registry.ts").Command<z.ZodObject<{}, z
12
12
  }>;
13
13
  /**
14
14
  * A new profile, put in use unless asked not to; while a game goes on it is made all the same and waits, the player in use staying. The one Chesspal made at
15
- * the first start, while new, becomes it, game and all (D-71).
15
+ * the first start, while new, becomes it, game and all (D-71). With the player's names on Lichess and Chess.com, their ratings are read there; their
16
+ * FIDE rating they type (D-106).
16
17
  */
17
18
  export declare const newProfile: import("./registry.ts").Command<z.ZodObject<{
18
19
  name: z.ZodString;
@@ -22,12 +23,9 @@ export declare const newProfile: import("./registry.ts").Command<z.ZodObject<{
22
23
  regular: "regular";
23
24
  tournament: "tournament";
24
25
  }>>;
25
- rating: z.ZodOptional<z.ZodInt>;
26
- site: z.ZodOptional<z.ZodEnum<{
27
- lichess: "lichess";
28
- "chess.com": "chess.com";
29
- fide: "fide";
30
- }>>;
26
+ lichess: z.ZodOptional<z.ZodString>;
27
+ chesscom: z.ZodOptional<z.ZodString>;
28
+ fide: z.ZodOptional<z.ZodNumber>;
31
29
  avatar: z.ZodOptional<z.ZodEnum<{
32
30
  "short-hair": "short-hair";
33
31
  bun: "bun";
@@ -55,10 +53,21 @@ export declare const setAvatar: import("./registry.ts").Command<z.ZodObject<{
55
53
  profile: string;
56
54
  avatar: string;
57
55
  }>;
58
- /** The player's names on Lichess and chess.com, by which the games they load are theirs (D-93): each one given is written, an empty one goes. */
59
- export declare const setAccounts: import("./registry.ts").Command<z.ZodObject<{
56
+ /**
57
+ * What the player tells of themselves in the profile in use: their name, how they play, their names on Lichess and Chess.com, by which the games they
58
+ * load are theirs (D-93), and their FIDE rating. Each one given is written, an empty one goes; the profile's folder keeps its name (D-106).
59
+ */
60
+ export declare const editProfile: import("./registry.ts").Command<z.ZodObject<{
61
+ name: z.ZodOptional<z.ZodString>;
62
+ level: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
63
+ learning: "learning";
64
+ casual: "casual";
65
+ regular: "regular";
66
+ tournament: "tournament";
67
+ }>, z.ZodLiteral<"">]>>;
60
68
  lichess: z.ZodOptional<z.ZodString>;
61
69
  chesscom: z.ZodOptional<z.ZodString>;
70
+ fide: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"">]>>;
62
71
  }, z.core.$strip>, PlayerProfile>;
63
72
  /** A drawn avatar chosen for a profile: the player's own image goes, or it would still show. */
64
73
  export declare function chooseDrawnAvatar(context: Context, id: string, avatar: DrawnAvatar): void;
@@ -67,3 +76,8 @@ export declare function chooseDrawnAvatar(context: Context, id: string, avatar:
67
76
  * system's account until the player completes it (D-71).
68
77
  */
69
78
  export declare function playerLine(context: Context): string | undefined;
79
+ /**
80
+ * How the AI reads the player's ratings, two facts apart, never merged (D-106): their FIDE rating as they typed it, and the one a site gives, as it gives it,
81
+ * with its speed and the month of the last game there; none without either.
82
+ */
83
+ export declare function ratingsText({ fide, rating }: PlayerProfile): string | undefined;
@@ -1,10 +1,11 @@
1
- // The players' profiles (D-23), on the command line and the board page: listed, made, put in use, with their picture (D-45). The AI reads only who plays.
1
+ // The players' profiles (D-23), on the command line and the board page: listed, made, changed, put in use, with their picture (D-45). The AI reads only who plays.
2
2
  import { readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { endOf } from "../games/index.js";
6
- import { activateProfile, activeProfile, createProfile, drawnAvatars, gameFolder, levels, listProfiles, nameLimit, ratingLimits, ratingSites, removeOwnImage, saveOwnImage, updateProfile, } from "../profiles/index.js";
6
+ import { activateProfile, activeProfile, createProfile, drawnAvatars, fideHighest, fideLowest, gameFolder, levels, listProfiles, nameLimit, removeOwnImage, saveOwnImage, updateProfile, } from "../profiles/index.js";
7
7
  import { gameAt } from "./clock.js";
8
+ import { refreshRatings } from "./ratings.js";
8
9
  import { defineCommand } from "./registry.js";
9
10
  /** A change of profile refused while a game goes on: the page says it in the player's language. */
10
11
  export class GameGoingOn extends Error {
@@ -25,35 +26,33 @@ export const profiles = defineCommand({
25
26
  });
26
27
  /**
27
28
  * A new profile, put in use unless asked not to; while a game goes on it is made all the same and waits, the player in use staying. The one Chesspal made at
28
- * the first start, while new, becomes it, game and all (D-71).
29
+ * the first start, while new, becomes it, game and all (D-71). With the player's names on Lichess and Chess.com, their ratings are read there; their
30
+ * FIDE rating they type (D-106).
29
31
  */
30
32
  export const newProfile = defineCommand({
31
33
  name: "new_profile",
32
34
  description: "Makes a player's profile and puts it in use, unless --use false or a game is going on: their games, picture and page settings go in "
33
35
  + "profiles/<id>/ of Chesspal's data folder; the profile Chesspal made at the first start becomes this one, its folder renamed, while nobody has completed "
34
- + "it. Example: chesspal new_profile Anna --level regular --rating 1450 --site lichess.",
36
+ + "it; their rating is read from their names on Lichess and Chess.com, and their FIDE rating, if they have one, is typed. Example: chesspal new_profile "
37
+ + "Anna --level regular --lichess anna_k --fide 1650.",
35
38
  input: z.object({
36
- name: z.string().trim().min(1).max(nameLimit).regex(/^[^\p{Cc}]+$/u, "one line of text").describe("the player's name"),
39
+ name: playerName().describe("the player's name"),
37
40
  level: z.enum(levels).optional().describe("how they play: learning, casual, regular or tournament"),
38
- rating: z.int().min(ratingLimits.lowest).max(ratingLimits.highest).optional().describe("their rating, with site"),
39
- site: z.enum(ratingSites).optional().describe("where the rating comes from: lichess, chess.com or fide"),
41
+ lichess: accountName().optional().describe("the player's name on Lichess"),
42
+ chesscom: accountName().optional().describe("the player's name on Chess.com"),
43
+ fide: fideRating().optional().describe(`the player's FIDE rating, ${fideLowest} to ${fideHighest}`),
40
44
  avatar: z.enum(drawnAvatars).optional().describe(`a drawn avatar: ${drawnAvatars.join(", ")}`),
41
45
  use: z.boolean().optional().describe("false to make it without putting it in use"),
42
46
  }),
43
47
  cliOnly: true,
44
48
  positionals: ["name"],
45
- run: ({ name, level, rating, site, avatar, use }, context) => {
46
- if ((rating === undefined) !== (site === undefined)) {
47
- throw new Error("Give the rating and its site together, such as --rating 1450 --site lichess, or neither. Nothing changed.");
48
- }
49
+ run: async ({ name, level, lichess, chesscom, fide, avatar, use }, context) => {
49
50
  const first = activeProfile(context.home()).new === true;
50
51
  const inUse = first || (use !== false && !isPlaying(context));
51
- const profile = createProfile(context.home(), {
52
- name,
53
- ...(level === undefined ? {} : { level }),
54
- ...(rating === undefined || site === undefined ? {} : { rating: { value: rating, site } }),
55
- ...(avatar === undefined ? {} : { avatar }),
56
- }, inUse);
52
+ const fields = { name, ...(level === undefined ? {} : { level }), ...(avatar === undefined ? {} : { avatar }), ...(fide === undefined ? {} : { fide }) };
53
+ const made = createProfile(context.home(), fields, inUse);
54
+ const accounts = accountsOf(lichess ?? "", chesscom ?? "");
55
+ const profile = Object.keys(accounts).length === 0 ? made : await withRatings(context, updateProfile(context.home(), made.id, { accounts }));
57
56
  return { ...profile, inUse };
58
57
  },
59
58
  format: (profile) => `Profile ${profile.id} made${profile.inUse ? ", and in use" : ", not in use"}: ${playerText(profile)}.`,
@@ -98,24 +97,36 @@ export const setAvatar = defineCommand({
98
97
  },
99
98
  format: ({ profile, avatar }) => `The picture of ${profile} is ${avatar}.`,
100
99
  });
101
- /** The player's names on Lichess and chess.com, by which the games they load are theirs (D-93): each one given is written, an empty one goes. */
102
- export const setAccounts = defineCommand({
103
- name: "set_accounts",
104
- description: "Sets the player's names on Lichess and chess.com in the profile in use, by which a game loaded from there is theirs; an empty name removes it. "
105
- + "Example: chesspal set_accounts --lichess anna_k --chesscom AnnaK.",
100
+ /**
101
+ * What the player tells of themselves in the profile in use: their name, how they play, their names on Lichess and Chess.com, by which the games they
102
+ * load are theirs (D-93), and their FIDE rating. Each one given is written, an empty one goes; the profile's folder keeps its name (D-106).
103
+ */
104
+ export const editProfile = defineCommand({
105
+ name: "edit_profile",
106
+ description: "Changes the profile in use: the player's name, how they play, their names on Lichess and Chess.com, by which a game loaded from there is "
107
+ + "theirs, and their FIDE rating; an empty level, site name or FIDE rating removes it, and the profile keeps its folder. Example: chesspal edit_profile "
108
+ + "--name Anna --level regular --lichess anna_k --fide 1650.",
106
109
  input: z.object({
110
+ name: playerName().optional().describe("the player's name"),
111
+ level: z.union([z.enum(levels), z.literal("")]).optional().describe("how they play: learning, casual, regular or tournament; empty to remove it"),
107
112
  lichess: accountName().optional().describe("the player's name on Lichess"),
108
- chesscom: accountName().optional().describe("the player's name on chess.com"),
113
+ chesscom: accountName().optional().describe("the player's name on Chess.com"),
114
+ fide: z.union([fideRating(), z.literal("")]).optional().describe(`the player's FIDE rating, ${fideLowest} to ${fideHighest}; empty to remove it`),
109
115
  }),
110
116
  cliOnly: true,
111
117
  idempotent: true,
112
- run: (names, context) => {
118
+ run: async ({ name, level, lichess, chesscom, fide }, context) => {
113
119
  const profile = activeProfile(context.home());
114
- const lichess = names.lichess ?? profile.accounts?.lichess ?? "";
115
- const chesscom = names.chesscom ?? profile.accounts?.chesscom ?? "";
116
- return updateProfile(context.home(), profile.id, { accounts: { ...(lichess === "" ? {} : { lichess }), ...(chesscom === "" ? {} : { chesscom }) } });
120
+ const named = lichess !== undefined || chesscom !== undefined;
121
+ const edited = updateProfile(context.home(), profile.id, {
122
+ ...(name === undefined ? {} : { name }),
123
+ ...(level === undefined ? {} : { level: level === "" ? undefined : level }),
124
+ ...(fide === undefined ? {} : { fide: fide === "" ? undefined : fide }),
125
+ ...(named ? { accounts: accountsOf(lichess ?? profile.accounts?.lichess ?? "", chesscom ?? profile.accounts?.chesscom ?? "") } : {}),
126
+ });
127
+ return named ? withRatings(context, edited) : edited;
117
128
  },
118
- format: (profile) => `${profile.id} is ${accountsText(profile.accounts)}.`,
129
+ format: (profile) => `Profile ${profile.id}: ${playerText(profile)}; ${accountsText(profile.accounts)}.`,
119
130
  });
120
131
  /** A drawn avatar chosen for a profile: the player's own image goes, or it would still show. */
121
132
  export function chooseDrawnAvatar(context, id, avatar) {
@@ -130,6 +141,14 @@ export function playerLine(context) {
130
141
  const profile = activeProfile(context.home());
131
142
  return profile.new === true ? undefined : `Player: ${playerText(profile)}.`;
132
143
  }
144
+ /**
145
+ * How the AI reads the player's ratings, two facts apart, never merged (D-106): their FIDE rating as they typed it, and the one a site gives, as it gives it,
146
+ * with its speed and the month of the last game there; none without either.
147
+ */
148
+ export function ratingsText({ fide, rating }) {
149
+ const ratings = [...(fide === undefined ? [] : [`FIDE ${fide}`]), ...(rating === undefined ? [] : [ratingText(rating)])];
150
+ return ratings.length === 0 ? undefined : ratings.join(" and ");
151
+ }
133
152
  /** Throws while a game goes on in the profile in use. */
134
153
  function refuseWhilePlaying(context) {
135
154
  if (isPlaying(context)) {
@@ -141,17 +160,39 @@ function isPlaying(context) {
141
160
  return current !== undefined && endOf(current.game) === undefined;
142
161
  }
143
162
  const levelText = { learning: "has just learned", casual: "plays now and then", regular: "plays often", tournament: "plays tournaments" };
144
- const siteText = { "lichess": "Lichess", "chess.com": "Chess.com", "fide": "FIDE" };
145
- function playerText({ name, level, rating }) {
146
- return [name, ...(level === undefined ? [] : [levelText[level]]), ...(rating === undefined ? [] : [`${siteText[rating.site]} ${rating.value}`])].join(", ");
163
+ const siteText = { "lichess": "Lichess", "chess.com": "Chess.com" };
164
+ function playerText(profile) {
165
+ const { name, level } = profile;
166
+ const ratings = ratingsText(profile);
167
+ return [name, ...(level === undefined ? [] : [levelText[level]]), ...(ratings === undefined ? [] : [`rated ${ratings}`])].join(", ");
168
+ }
169
+ function ratingText({ value, speed, site, played }) {
170
+ return `${value} ${speed} on ${siteText[site]}${played === undefined ? "" : ` (last game ${played.slice(0, 7)})`}`;
171
+ }
172
+ /** The profile once its names on the sites have been read, when they changed: the command line and the page wait for the sites, at most a few seconds. */
173
+ async function withRatings(context, profile) {
174
+ const read = await refreshRatings(context, profile.id);
175
+ return read ? listProfiles(context.home()).find(({ id }) => id === profile.id) ?? profile : profile;
147
176
  }
148
- /** A name on Lichess or chess.com: their letters, digits, underscores and hyphens; empty to remove it. */
177
+ /** The player's name: one line of text. */
178
+ function playerName() {
179
+ return z.string().trim().min(1).max(nameLimit).regex(/^[^\p{Cc}]+$/u, "one line of text");
180
+ }
181
+ /** A FIDE rating: a whole number in FIDE's range. */
182
+ function fideRating() {
183
+ const range = `A FIDE rating is a whole number from ${fideLowest}, FIDE's lowest since 2024, to ${fideHighest}.`;
184
+ return z.number().int(range).min(fideLowest, range).max(fideHighest, range);
185
+ }
186
+ /** A name on Lichess or Chess.com: their letters, digits, underscores and hyphens; empty to remove it. */
149
187
  function accountName() {
150
- return z.string().trim().max(nameLimit).regex(/^[\w-]*$/, "A name on Lichess or chess.com has only letters, digits, underscores and hyphens.");
188
+ return z.string().trim().max(nameLimit).regex(/^[\w-]*$/, "A name on Lichess or Chess.com has only letters, digits, underscores and hyphens.");
189
+ }
190
+ function accountsOf(lichess, chesscom) {
191
+ return { ...(lichess === "" ? {} : { lichess }), ...(chesscom === "" ? {} : { chesscom }) };
151
192
  }
152
193
  function accountsText(accounts) {
153
- const named = [...(accounts?.lichess === undefined ? [] : [`${accounts.lichess} on Lichess`]), ...(accounts?.chesscom === undefined ? [] : [`${accounts.chesscom} on chess.com`])];
154
- return named.length === 0 ? "on neither Lichess nor chess.com" : named.join(" and ");
194
+ const named = [...(accounts?.lichess === undefined ? [] : [`${accounts.lichess} on Lichess`]), ...(accounts?.chesscom === undefined ? [] : [`${accounts.chesscom} on Chess.com`])];
195
+ return named.length === 0 ? "on neither Lichess nor Chess.com" : named.join(" and ");
155
196
  }
156
197
  function imageFile(path) {
157
198
  try {
@@ -0,0 +1,8 @@
1
+ import type { Context } from "./context.ts";
2
+ /** A day, in milliseconds: the page reads the ratings again once they are older. */
3
+ export declare const ratingsAge: number;
4
+ /**
5
+ * Reads a profile's ratings again when its names on the sites changed, a site did not answer ten minutes ago or more, or the last reading is older than `olderThan`
6
+ * milliseconds: whether new readings were kept. One reading at a time of the same names in this process.
7
+ */
8
+ export declare function refreshRatings(context: Context, id: string, olderThan?: number): Promise<boolean>;