chesspal 0.2.4 → 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 (183) 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 +39 -13
  8. package/dist/commands/ai-say.js +3 -4
  9. package/dist/commands/analyse-game.d.ts +4 -0
  10. package/dist/commands/analyse-game.js +46 -13
  11. package/dist/commands/annotate-game.d.ts +2 -0
  12. package/dist/commands/annotate-game.js +40 -18
  13. package/dist/commands/answer-lines.d.ts +2 -0
  14. package/dist/commands/answer-lines.js +13 -3
  15. package/dist/commands/coach.js +4 -2
  16. package/dist/commands/context.d.ts +9 -1
  17. package/dist/commands/context.js +5 -0
  18. package/dist/commands/delete-review.js +9 -5
  19. package/dist/commands/game-summary.d.ts +2 -0
  20. package/dist/commands/game-summary.js +4 -0
  21. package/dist/commands/index.d.ts +3 -2
  22. package/dist/commands/index.js +7 -4
  23. package/dist/commands/init.js +2 -4
  24. package/dist/commands/instructions.d.ts +9 -0
  25. package/dist/commands/instructions.js +30 -0
  26. package/dist/commands/lead-input.d.ts +1 -1
  27. package/dist/commands/lead-input.js +7 -4
  28. package/dist/commands/page-link.js +6 -5
  29. package/dist/commands/past-games.d.ts +1 -1
  30. package/dist/commands/past-games.js +12 -5
  31. package/dist/commands/player-move.js +2 -3
  32. package/dist/commands/profiles.d.ts +23 -9
  33. package/dist/commands/profiles.js +76 -35
  34. package/dist/commands/ratings.d.ts +8 -0
  35. package/dist/commands/ratings.js +45 -0
  36. package/dist/commands/registry.d.ts +1 -1
  37. package/dist/commands/registry.js +3 -2
  38. package/dist/commands/say.d.ts +1 -1
  39. package/dist/commands/say.js +1 -1
  40. package/dist/commands/status.d.ts +3 -1
  41. package/dist/commands/status.js +46 -27
  42. package/dist/commands/study.js +8 -5
  43. package/dist/core/index.d.ts +1 -1
  44. package/dist/core/index.js +1 -1
  45. package/dist/core/review.d.ts +2 -0
  46. package/dist/core/review.js +5 -1
  47. package/dist/games/ai-model.d.ts +4 -2
  48. package/dist/games/ai-model.js +10 -1
  49. package/dist/games/analysis.d.ts +26 -3
  50. package/dist/games/analysis.js +41 -6
  51. package/dist/games/archive.d.ts +6 -3
  52. package/dist/games/archive.js +13 -5
  53. package/dist/games/current-game.d.ts +10 -1
  54. package/dist/games/current-game.js +49 -15
  55. package/dist/games/files.d.ts +5 -0
  56. package/dist/games/files.js +42 -1
  57. package/dist/games/index.d.ts +3 -3
  58. package/dist/games/index.js +3 -3
  59. package/dist/home/data-folder.d.ts +2 -0
  60. package/dist/home/data-folder.js +5 -1
  61. package/dist/home/index.d.ts +1 -1
  62. package/dist/home/index.js +1 -1
  63. package/dist/host-files/agents.d.ts +2 -2
  64. package/dist/host-files/agents.js +12 -6
  65. package/dist/host-files/host-files.d.ts +5 -3
  66. package/dist/host-files/host-files.js +15 -8
  67. package/dist/host-files/index.d.ts +2 -2
  68. package/dist/host-files/index.js +2 -2
  69. package/dist/page/app/app.js +29 -16
  70. package/dist/page/app/title.js +17 -0
  71. package/dist/page/avatars/torretta.js +4 -4
  72. package/dist/page/dialogs/analyses.js +4 -4
  73. package/dist/page/dialogs/dialog.js +4 -1
  74. package/dist/page/dialogs/dialogs.css +84 -10
  75. package/dist/page/dialogs/profiles.js +95 -42
  76. package/dist/page/game/column.js +5 -1
  77. package/dist/page/game/plies.js +7 -4
  78. package/dist/page/guide/blocks.js +67 -0
  79. package/dist/page/guide/en.md +66 -0
  80. package/dist/page/guide/foot.js +18 -0
  81. package/dist/page/guide/guide.css +200 -0
  82. package/dist/page/guide/guide.js +89 -0
  83. package/dist/page/guide/it.md +66 -0
  84. package/dist/page/guide/markdown.js +67 -0
  85. package/dist/page/helps/helps.css +7 -10
  86. package/dist/page/layout/favicon.svg +13 -0
  87. package/dist/page/layout/header.js +3 -3
  88. package/dist/page/layout/icon.icns +0 -0
  89. package/dist/page/layout/icon.ico +0 -0
  90. package/dist/page/layout/icon.png +0 -0
  91. package/dist/page/layout/layout.css +111 -4
  92. package/dist/page/layout/window.js +45 -0
  93. package/dist/page/main.js +4 -1
  94. package/dist/page/players/card.js +1 -1
  95. package/dist/page/profile/profile.js +14 -9
  96. package/dist/page/profile/settings.js +9 -0
  97. package/dist/page/style.css +2 -0
  98. package/dist/page/table/opener.css +65 -0
  99. package/dist/page/table/opener.js +59 -0
  100. package/dist/page/talk/chat.css +10 -3
  101. package/dist/page/talk/talk.css +38 -26
  102. package/dist/page/talk/talk.js +4 -2
  103. package/dist/page/text.js +83 -17
  104. package/dist/page/theme.css +7 -0
  105. package/dist/profiles/index.d.ts +2 -1
  106. package/dist/profiles/index.js +2 -1
  107. package/dist/profiles/profiles.d.ts +10 -15
  108. package/dist/profiles/profiles.js +30 -15
  109. package/dist/profiles/ratings.d.ts +35 -0
  110. package/dist/profiles/ratings.js +61 -0
  111. package/dist/server/bell.d.ts +24 -3
  112. package/dist/server/bell.js +84 -10
  113. package/dist/server/caller.d.ts +2 -1
  114. package/dist/server/caller.js +3 -2
  115. package/dist/server/mcp.d.ts +7 -3
  116. package/dist/server/mcp.js +100 -52
  117. package/dist/server/page/analyses.d.ts +2 -0
  118. package/dist/server/page/analyses.js +20 -5
  119. package/dist/server/page/attach.d.ts +4 -0
  120. package/dist/server/page/attach.js +112 -0
  121. package/dist/server/page/chat.d.ts +60 -0
  122. package/dist/server/page/chat.js +79 -0
  123. package/dist/server/page/events.d.ts +7 -0
  124. package/dist/server/page/events.js +20 -0
  125. package/dist/server/page/files.js +10 -1
  126. package/dist/server/page/game.js +43 -23
  127. package/dist/server/page/page.d.ts +47 -7
  128. package/dist/server/page/page.js +69 -48
  129. package/dist/server/page/profiles.d.ts +5 -0
  130. package/dist/server/page/profiles.js +26 -10
  131. package/dist/server/page/table.d.ts +39 -0
  132. package/dist/server/page/table.js +102 -0
  133. package/dist/server/table/browser.d.ts +6 -0
  134. package/dist/server/table/browser.js +48 -0
  135. package/dist/server/table/hosts/chats.d.ts +7 -0
  136. package/dist/server/table/hosts/chats.js +31 -0
  137. package/dist/server/table/hosts/find.d.ts +2 -0
  138. package/dist/server/table/hosts/find.js +20 -0
  139. package/dist/server/table/hosts/hosts.d.ts +34 -0
  140. package/dist/server/table/hosts/hosts.js +49 -0
  141. package/dist/server/table/hosts/index.d.ts +3 -0
  142. package/dist/server/table/hosts/index.js +4 -0
  143. package/dist/server/table/hosts/play.d.ts +25 -0
  144. package/dist/server/table/hosts/play.js +152 -0
  145. package/dist/server/table/hosts/seat.d.ts +25 -0
  146. package/dist/server/table/hosts/seat.js +74 -0
  147. package/dist/server/table/hosts/terminal.d.ts +6 -0
  148. package/dist/server/table/hosts/terminal.js +63 -0
  149. package/dist/server/table/icon/index.d.ts +32 -0
  150. package/dist/server/table/icon/index.js +83 -0
  151. package/dist/server/table/icon/linux.d.ts +5 -0
  152. package/dist/server/table/icon/linux.js +47 -0
  153. package/dist/server/table/icon/macos.d.ts +3 -0
  154. package/dist/server/table/icon/macos.js +38 -0
  155. package/dist/server/table/icon/windows.d.ts +3 -0
  156. package/dist/server/table/icon/windows.js +48 -0
  157. package/dist/server/table/index.d.ts +6 -0
  158. package/dist/server/table/index.js +7 -0
  159. package/dist/server/table/launch.d.ts +19 -0
  160. package/dist/server/table/launch.js +78 -0
  161. package/dist/server/table/link.d.ts +10 -0
  162. package/dist/server/table/link.js +230 -0
  163. package/dist/server/table/table.d.ts +35 -0
  164. package/dist/server/table/table.js +94 -0
  165. package/dist/sites/chesscom.d.ts +4 -0
  166. package/dist/sites/chesscom.js +25 -0
  167. package/dist/sites/choice.d.ts +17 -0
  168. package/dist/sites/choice.js +13 -0
  169. package/dist/sites/index.d.ts +2 -0
  170. package/dist/sites/index.js +4 -0
  171. package/dist/sites/lichess.d.ts +4 -0
  172. package/dist/sites/lichess.js +32 -0
  173. package/dist/sites/request.d.ts +11 -0
  174. package/dist/sites/request.js +15 -0
  175. package/dist/sites/sites.d.ts +6 -0
  176. package/dist/sites/sites.js +33 -0
  177. package/instructions/analysis.md +1 -1
  178. package/instructions/analyst-agent.md +1 -1
  179. package/instructions/coach.md +2 -1
  180. package/instructions/opponent.md +6 -6
  181. package/instructions/ponder.md +1 -0
  182. package/instructions/start.md +1 -0
  183. package/package.json +1 -1
@@ -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
+ }
@@ -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
  });
@@ -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>;
@@ -0,0 +1,45 @@
1
+ // The player's ratings read again from the sites (D-105, D-106): when their names there change, and when the page opens a day after the last reading;
2
+ // never from a tool of the AI's.
3
+ import { listProfiles, profileFolder, saveReadings } from "../profiles/index.js";
4
+ import { readingsStale, readSites } from "../sites/index.js";
5
+ /** A day, in milliseconds: the page reads the ratings again once they are older. */
6
+ export const ratingsAge = 24 * 60 * 60 * 1000;
7
+ /**
8
+ * 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`
9
+ * milliseconds: whether new readings were kept. One reading at a time of the same names in this process.
10
+ */
11
+ export function refreshRatings(context, id, olderThan = Infinity) {
12
+ const home = context.home();
13
+ const profile = listProfiles(home).find((one) => one.id === id);
14
+ if (profile === undefined || context.fetch === undefined) {
15
+ return Promise.resolve(false);
16
+ }
17
+ const key = [home, id, profile.accounts?.lichess, profile.accounts?.chesscom].join("\n");
18
+ const going = reading.get(key);
19
+ if (going !== undefined) {
20
+ return going;
21
+ }
22
+ const read = readAgain(context, profile, context.fetch, olderThan).finally(() => reading.delete(key));
23
+ reading.set(key, read);
24
+ return read;
25
+ }
26
+ const reading = new Map();
27
+ async function readAgain(context, { id, accounts, sites }, fetch, olderThan) {
28
+ const home = context.home();
29
+ const age = sites === undefined ? Infinity : context.now().getTime() - Date.parse(sites.read);
30
+ if (!readingsStale(sites, accounts, context.now()) && age < olderThan) {
31
+ return false;
32
+ }
33
+ if (accounts === undefined) {
34
+ saveReadings(profileFolder(home, id), undefined);
35
+ return true;
36
+ }
37
+ const read = await readSites(accounts, fetch, sites, context.now());
38
+ // The names may have changed while the sites answered: what they said of the names before is not kept, and the change reads its own.
39
+ const now = listProfiles(home).find((one) => one.id === id);
40
+ if (now?.accounts?.lichess !== accounts.lichess || now?.accounts?.chesscom !== accounts.chesscom) {
41
+ return false;
42
+ }
43
+ saveReadings(profileFolder(home, id), read);
44
+ return true;
45
+ }
@@ -55,5 +55,5 @@ export declare function isBoolean(field: z.ZodType): boolean;
55
55
  export declare function isList(field: z.ZodType): boolean;
56
56
  /** The type of a field of a command's input, past optional, default and nullable: string, boolean, array… */
57
57
  export declare function baseType(field: z.ZodType): string;
58
- /** Whether a field takes numbers, alone or in a list: the command line reads them as text and turns them into numbers. */
58
+ /** Whether a field takes numbers, alone, in a list or as one of its choices: the command line reads them as text and turns them into numbers. */
59
59
  export declare function takesNumbers(field: z.ZodType): boolean;
@@ -37,10 +37,11 @@ export function isList(field) {
37
37
  export function baseType(field) {
38
38
  return baseDefinition(field).type;
39
39
  }
40
- /** Whether a field takes numbers, alone or in a list: the command line reads them as text and turns them into numbers. */
40
+ /** Whether a field takes numbers, alone, in a list or as one of its choices: the command line reads them as text and turns them into numbers. */
41
41
  export function takesNumbers(field) {
42
42
  const definition = baseDefinition(field);
43
- return definition.type === "number" || (definition.type === "array" && definition.element !== undefined && baseType(definition.element) === "number");
43
+ const numbers = (one) => one !== undefined && baseType(one) === "number";
44
+ return definition.type === "number" || (definition.type === "array" && numbers(definition.element)) || (definition.type === "union" && (definition.options ?? []).some(numbers));
44
45
  }
45
46
  function inputIssue(command, issue, given, written) {
46
47
  const path = issue.path.map(String).join(".");
@@ -9,7 +9,7 @@ export declare function say(folder: string, who: Sayer, text: string, at: Date,
9
9
  /** Words of the player's as the AI is given them: their text, and in analysis the move the board page showed as they wrote, 52...♚f6 (D-101). */
10
10
  export interface Written {
11
11
  readonly text: string;
12
- readonly at?: string;
12
+ readonly place?: string;
13
13
  }
14
14
  /** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once; never to an agent (D-89). */
15
15
  export declare function newWords(context: Context): Written[];
@@ -57,7 +57,7 @@ export function give(context, words) {
57
57
  context.heard?.();
58
58
  }
59
59
  const analysed = words.some((word) => word.shown !== undefined) ? gameInAnalysis(gameFolder(context.home()))?.game : undefined;
60
- return words.map(({ text, shown }) => shown === undefined ? { text } : { text, at: placeOf(shown, analysed) });
60
+ return words.map(({ text, shown }) => shown === undefined ? { text } : { text, place: placeOf(shown, analysed) });
61
61
  }
62
62
  /** Where the page stood: the move shown, as the player saw it, or the start; with the game's name when the page shows another now. */
63
63
  function placeOf({ game, move }, analysed) {
@@ -22,7 +22,7 @@ interface Status {
22
22
  analysis?: {
23
23
  game: string;
24
24
  brought?: true;
25
- analyst?: true;
25
+ analyst?: "done" | "offer";
26
26
  review?: string;
27
27
  };
28
28
  /** A game going on from before this chat, told once a chat: the player says whether to go on with it. */
@@ -31,6 +31,8 @@ interface Status {
31
31
  coach?: true;
32
32
  /** The files an older init wrote in the player's folder, which this version would write otherwise. */
33
33
  outdated?: string[];
34
+ /** The chat has not read the opponent's instructions since it started, or was cleared or summarized (D-102). */
35
+ unread?: true;
34
36
  }
35
37
  /** What Chesspal sees now, the first call of a chat: a dashboard under 150 tokens while the player is to move, with the next step. */
36
38
  export declare const status: import("./registry.ts").Command<z.ZodObject<{}, z.core.$strip>, Status>;