chesspal 0.3.3 → 0.3.5

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 (61) hide show
  1. package/README.md +49 -10
  2. package/dist/commands/analysis/analyse.d.ts +4 -1
  3. package/dist/commands/analysis/analyse.js +46 -12
  4. package/dist/commands/analysis/index.d.ts +1 -0
  5. package/dist/commands/analysis/index.js +2 -1
  6. package/dist/commands/analysis/site-games.d.ts +27 -0
  7. package/dist/commands/analysis/site-games.js +84 -0
  8. package/dist/commands/index.d.ts +1 -1
  9. package/dist/commands/index.js +3 -2
  10. package/dist/commands/profiles/index.d.ts +1 -1
  11. package/dist/commands/profiles/index.js +1 -1
  12. package/dist/commands/profiles/profiles.d.ts +3 -1
  13. package/dist/commands/profiles/profiles.js +2 -1
  14. package/dist/commands/registry/context.d.ts +4 -1
  15. package/dist/commands/registry/registry.d.ts +2 -0
  16. package/dist/core/accounts.d.ts +11 -0
  17. package/dist/core/accounts.js +14 -0
  18. package/dist/core/index.d.ts +1 -0
  19. package/dist/core/index.js +1 -0
  20. package/dist/page/app/app.js +10 -9
  21. package/dist/page/dialogs/analyses.js +7 -17
  22. package/dist/page/dialogs/dialogs.css +102 -0
  23. package/dist/page/dialogs/outcome.js +13 -0
  24. package/dist/page/dialogs/site-games.js +71 -0
  25. package/dist/page/dialogs/studied.js +13 -19
  26. package/dist/page/dialogs/study.js +30 -13
  27. package/dist/page/guide/en.md +1 -0
  28. package/dist/page/guide/it.md +1 -0
  29. package/dist/page/helps/helps.css +26 -18
  30. package/dist/page/helps/helps.js +2 -2
  31. package/dist/page/text.js +32 -2
  32. package/dist/server/mcp/mcp.js +2 -2
  33. package/dist/server/page/analyses.d.ts +5 -0
  34. package/dist/server/page/analyses.js +18 -1
  35. package/dist/server/page/game.js +3 -3
  36. package/dist/server/page/page.js +4 -1
  37. package/dist/sites/chesscom.d.ts +1 -1
  38. package/dist/sites/chesscom.js +1 -1
  39. package/dist/sites/games/chesscom.d.ts +4 -0
  40. package/dist/sites/games/chesscom.js +54 -0
  41. package/dist/sites/games/game.d.ts +34 -0
  42. package/dist/sites/games/game.js +64 -0
  43. package/dist/sites/games/games.d.ts +14 -0
  44. package/dist/sites/games/games.js +22 -0
  45. package/dist/sites/games/index.d.ts +2 -0
  46. package/dist/sites/games/index.js +3 -0
  47. package/dist/sites/games/lichess.d.ts +4 -0
  48. package/dist/sites/games/lichess.js +45 -0
  49. package/dist/sites/index.d.ts +2 -1
  50. package/dist/sites/index.js +5 -3
  51. package/dist/sites/lichess.d.ts +1 -1
  52. package/dist/sites/lichess.js +1 -1
  53. package/dist/sites/request/index.d.ts +1 -0
  54. package/dist/sites/request/index.js +2 -0
  55. package/dist/sites/{request.d.ts → request/request.d.ts} +7 -0
  56. package/dist/sites/request/request.js +37 -0
  57. package/dist/sites/sites.d.ts +1 -1
  58. package/dist/sites/sites.js +1 -1
  59. package/instructions/opponent.md +1 -1
  60. package/package.json +1 -1
  61. package/dist/sites/request.js +0 -15
package/README.md CHANGED
@@ -6,21 +6,60 @@ Play chess against an AI on a real board, or learn with one.
6
6
 
7
7
  ## Get started
8
8
 
9
- You need [Node.js](https://nodejs.org) 22.13 or newer and an AI agent on your computer, [Claude Code](https://code.claude.com/docs/en/setup) or [Codex](https://developers.openai.com/codex), signed in with your own subscription: see [what you need](#what-you-need).
9
+ Five steps, done once, in about 15 minutes. You need a paid Claude plan or a ChatGPT account. After these steps you open the board with a double click.
10
10
 
11
- Open a terminal, the window where you type commands, after installing Node.js: one opened before does not find `npm`. On Windows, open Command Prompt (press Start, type `cmd`, press Enter): PowerShell, on a new computer, refuses npm's commands with "running scripts is disabled on this system", until you type `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` there once and answer yes. On macOS, open Terminal. Then type:
11
+ **1. Install Node.js.** Download it from [nodejs.org](https://nodejs.org): take the version marked **LTS**, the steady one (22.13 or newer). Install it like any program, keeping every choice as it is; on Windows, leave the box "Automatically install the necessary tools" unticked.
12
+
13
+ **2. Open a terminal**, the window where you type commands. Open it after installing Node.js: one opened before does not find it.
14
+
15
+ - **Windows:** press Start, type `cmd` and press Enter. This opens Command Prompt. Use it rather than PowerShell: on a new computer PowerShell refuses these commands with "running scripts is disabled on this system".
16
+ - **macOS:** press ⌘ and Space, type `Terminal` and press Enter.
17
+
18
+ In the steps below, copy each command, paste it in the terminal (Ctrl+V or right-click; on macOS ⌘V) and press Enter. Wait for the terminal to finish before the next one. Lines with "WARN" are normal: go on.
19
+
20
+ **3. Install the AI and sign in.** Choose one of the two. You need your own subscription: Chesspal never uses an API key, which is paid by the call.
21
+
22
+ - **Claude Code**, with a paid Claude plan (Pro or higher; the free plan does not include it):
23
+
24
+ ```bash
25
+ npm install -g @anthropic-ai/claude-code
26
+ claude
27
+ ```
28
+
29
+ `claude` asks a few questions first, such as the colours you prefer: press Enter to keep the first choice. Then it opens your browser: sign in with your Claude account and go back to the terminal.
30
+ - **Codex**, with a ChatGPT account:
31
+
32
+ ```bash
33
+ npm install -g @openai/codex
34
+ codex
35
+ ```
36
+
37
+ Choose **Sign in with ChatGPT** and sign in in your browser.
38
+
39
+ Once you are signed in, close that terminal window with its × and open a new one, as in step 2.
40
+
41
+ **4. Install Chesspal and open it.**
12
42
 
13
43
  ```bash
14
44
  npm install -g chesspal
15
45
  chesspal
16
46
  ```
17
47
 
18
- - `npm install -g` installs the `chesspal` command for the whole computer, where your agent finds it.
19
- - `chesspal` opens the board page in your browser. The first time, it offers an icon on your desktop and in your menu of programs: with it, you open the board with a double click, with no terminal.
20
- - The first time, the board page asks who you are: fill it in and press **Start playing**.
21
- - Press **Claude Code** or **Codex**: the AI opens in a terminal of its own, at the table; keep it open while you play. The first time, it asks whether you trust the folder: choose **Yes** with the arrow keys and press Enter. With Codex, when it asks about Chesspal's two hooks, choose **Trust all and continue**; on Windows, it also offers to set up its sandbox: choose the first option, and answer yes when Windows asks for permission.
48
+ The board page opens in your browser, and you can close this terminal. The first time, Chesspal offers an icon on your desktop and in your menu of programs: accept it, and from then on you open the board with a double click on it, with no terminal.
49
+
50
+ **5. Start the AI.** The first time, the board page asks who you are: fill it in and press **Start playing**. Then press **Claude Code** or **Codex**: the AI opens in a terminal of its own. Keep that window open while you play. The first time it asks some questions:
51
+
52
+ - whether you trust the folder: choose **Yes** with the arrow keys and press Enter;
53
+ - with Codex, about Chesspal's two hooks, the small programs that tell the AI when you move on the board: choose **Trust all and continue**;
54
+ - with Codex on Windows, whether to set up its sandbox, a safe space where it works: choose the first option, and answer yes when Windows asks for permission.
55
+
56
+ Now press **New game**: choose your colour, the clock and the AI's level, and play on the board. Talk with the AI on the board page or in its terminal.
57
+
58
+ **If a command fails:**
22
59
 
23
- Then press **New game**: you choose your colour, the clock and the AI's level, and play on the board. Talk with the AI on the board page or in its terminal.
60
+ - "npm is not recognized" or "command not found": close the terminal and open a new one; if it still fails, install Node.js again.
61
+ - On macOS, "EACCES" or "permission denied": npm cannot write where Node.js put it. Follow [npm's guide](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally), not `sudo`: with `sudo`, Chesspal and Claude Code cannot update themselves.
62
+ - Anything else in red, or with "ERR!": copy the last lines and [tell us](#try-it-and-tell-us).
24
63
 
25
64
  ## What it does
26
65
 
@@ -36,11 +75,11 @@ Coming later: an AI coach that helps you grow, exercises chosen on your weak poi
36
75
 
37
76
  ## What you need
38
77
 
39
- - **Node.js 22.13** or newer: download the version marked LTS and install it like any program.
40
- - **An AI agent** with your own subscription: Claude Code, with a paid Claude plan, or Codex, with a ChatGPT account. Chesspal never uses an API key, which is paid by the call: sign your agent in with your account.
78
+ - **Node.js 22.13** or newer.
79
+ - **An AI agent** with your own subscription: [Claude Code](https://code.claude.com/docs/en/setup), with a paid Claude plan, or [Codex](https://developers.openai.com/codex), with a ChatGPT account.
41
80
  - **A web browser**, for the board page.
42
81
 
43
- Chesspal works with any agent that supports MCP tools and can work with the files on your computer: we develop it with Claude Code, and Codex is connected too.
82
+ Chesspal works with any agent that can use Chesspal's tools (MCP) and the files on your computer: we develop it with Claude Code, and Codex is connected too.
44
83
 
45
84
  ## Good to know
46
85
 
@@ -63,7 +63,10 @@ interface Analysis {
63
63
  /** The AI's word on the whole game, from annotate_game. */
64
64
  review?: string;
65
65
  }
66
- /** A game over move by move, for the analyst (D-68): the current one, one of the archive or kept to study, or one the player brings as a PGN, kept to study. */
66
+ /**
67
+ * A game over move by move, for the analyst (D-68): the current one, one of the archive or kept to study, one the player brings as a PGN, or one of their latest
68
+ * on the sites (D-110), kept to study.
69
+ */
67
70
  export declare const analyseGame: import("../registry/registry.ts").Command<z.ZodObject<{
68
71
  game: z.ZodOptional<z.ZodString>;
69
72
  pgn: z.ZodOptional<z.ZodString>;
@@ -1,37 +1,53 @@
1
1
  // The analyst's tool (D-21, D-68): a game over, move by move in one call, facts only and never a judgment, with the analyst's instructions once in a chat.
2
2
  import { z } from "zod";
3
- import { clockOfComment, clockText, closingOf, Game, gameReviewOf, isReviewed, leadOf, leadText, levelOf, materialOf, readPgn, reviewed, reviewOf, timeControlOfTag, timeControlText, withGameReview, withLead, } from "../../core/index.js";
3
+ import { accountSide, clockOfComment, clockText, closingOf, Game, gameReviewOf, isReviewed, leadOf, leadText, levelOf, materialOf, readPgn, reviewed, reviewOf, timeControlOfTag, timeControlText, withGameReview, withLead, } from "../../core/index.js";
4
4
  import { gameInAnalysis, isGameName, keepToStudy, lastChatStart, planOf, rewriteGame, startAnalysis } from "../../games/index.js";
5
5
  import { analystAgent, instructionsFile } from "../../host-files/index.js";
6
6
  import { activeProfile, gameFolder } from "../../profiles/index.js";
7
+ import { pointsAtSite, siteGameName, siteOfGameName } from "../../sites/index.js";
7
8
  import { analystOf, anotherAnalyst, balanceText, bookOpening, bookReaches, colourName, gameToStudy, knownTag, moveNumbers, namedGame, noGameGoingOn, riskLine, speechLine, speechOf, tacticsText } from "../answers/index.js";
8
- import { playerLine } from "../profiles/index.js";
9
+ import { playerLine, siteText } from "../profiles/index.js";
9
10
  import { defineCommand } from "../registry/index.js";
10
- /** A game over move by move, for the analyst (D-68): the current one, one of the archive or kept to study, or one the player brings as a PGN, kept to study. */
11
+ import { profileSiteGames } from "./site-games.js";
12
+ /**
13
+ * A game over move by move, for the analyst (D-68): the current one, one of the archive or kept to study, one the player brings as a PGN, or one of their latest
14
+ * on the sites (D-110), kept to study.
15
+ */
11
16
  export const analyseGame = defineCommand({
12
17
  name: "analyse_game",
13
18
  description: "Gives a game that is over move by move, to comment it with the player: each move with what it took and the time it took, the material "
14
19
  + "when it changes, the pieces at risk, the only legal move when there is one alone, the mates in one and what the next moves would do after it (forks, double attacks, discovered checks, "
15
20
  + "promotions, stalemates and stalemate traps), where the game left the opening book, and your comments and plans. Facts "
16
- + "only: the judgment is yours. Without game or pgn, the game just over, or else the last one played; game: a name as past_games or study_games "
17
- + "writes it; pgn: a game the player brings from elsewhere, kept to study in the profile. Never while a game goes on. The first time in a chat "
21
+ + "only: the judgment is yours. Without game or pgn, the game just over, or else the last one played; game: a name as past_games, study_games or "
22
+ + "site_games writes it; pgn: a game the player brings from elsewhere, kept to study in the profile. Never while a game goes on. The first time in a chat "
18
23
  + "it also says how to analyse. annotate_game then keeps your badges and notes for the board page. Example: analyse_game {}.",
19
24
  input: z.object({
20
- game: z.string().optional().describe("a game's name, as past_games, study_games or analyse_game writes it"),
25
+ game: z.string().optional().describe("a game's name, as past_games, study_games, site_games or analyse_game writes it"),
21
26
  pgn: z.string().optional().describe("a game played elsewhere, in PGN: its tags and its moves, with their [%clk] and a review exported from the board page if it has them; other comments and evals can go"),
22
27
  }),
23
28
  sharedWith: [analystAgent],
24
- run: ({ game, pgn }, context) => {
29
+ openWorld: true,
30
+ run: async ({ game, pgn }, context) => {
25
31
  if (game !== undefined && pgn !== undefined) {
26
32
  throw new Error("Give game or pgn, not both: game names a game of the archive, pgn brings one played elsewhere.");
27
33
  }
34
+ const given = game?.trim();
35
+ // A link, or a name of a site's game that site_games does not write, is no game Chesspal can find (D-110).
36
+ if ([given, pgn].some((text) => text !== undefined && pointsAtSite(text))) {
37
+ throw new Error("Chesspal takes no link: site_games lists the player's latest games on Lichess and Chess.com; give the name it writes, as game.");
38
+ }
28
39
  // A game's name given as pgn, as an AI often gives it, is that game (Priya).
29
- const named = pgn !== undefined && isGameName(pgn) ? pgn.trim() : game;
30
- const studied = pgn === undefined || named !== game ? gameToStudy(context, named) : importedGame(context, pgn);
40
+ const fromSite = siteGameName(given ?? pgn ?? "");
41
+ const named = pgn !== undefined && isGameName(pgn) ? pgn.trim() : given;
42
+ const studied = fromSite !== undefined
43
+ ? importedGame(context, await siteGamePgn(context, fromSite))
44
+ : pgn === undefined || named !== given
45
+ ? gameToStudy(context, named)
46
+ : importedGame(context, pgn);
31
47
  if (context.agent === analystAgent) {
32
48
  oneAnalyst(context, studied);
33
49
  }
34
- const analysis = analysisOf(studied, activeProfile(context.home()).name);
50
+ const analysis = analysisOf(studied, activeProfile(context.home()));
35
51
  if (studied.name !== undefined) {
36
52
  markBook(context, { record: studied.record, name: studied.name }, analysis.moves);
37
53
  startAnalysis(gameFolder(context.home()), studied.name, context.now());
@@ -111,6 +127,23 @@ function importedGame(context, pgn) {
111
127
  // A game brought again is the one already kept, with the review written on it since.
112
128
  return namedGame(context, keepToStudy(gameFolder(context.home()), record, context.now()));
113
129
  }
130
+ /** The PGN of one of the player's latest games on the sites, asked of them again by its name as site_games writes it (D-110). */
131
+ async function siteGamePgn(context, name) {
132
+ noGameGoingOn(context);
133
+ const site = siteOfGameName(name);
134
+ const profile = activeProfile(context.home()).id;
135
+ const { games, unreached } = await profileSiteGames(context, site);
136
+ if (activeProfile(context.home()).id !== profile) {
137
+ throw new Error("The profile in use changed while the site answered: call site_games again for the player's games now. Nothing changed.");
138
+ }
139
+ const found = games.find((one) => one.name === name);
140
+ if (found !== undefined) {
141
+ return found.pgn;
142
+ }
143
+ throw new Error(unreached.includes(site)
144
+ ? `${siteText[site]} could not be reached: tell the player, and ask again when they ask. Nothing changed.`
145
+ : `${name} is not among the player's latest games on ${siteText[site]} any more: call site_games again and give a name it writes. Nothing changed.`);
146
+ }
114
147
  /**
115
148
  * The opening book's moves marked as such in the game kept (D-69): a fact, not a judgment, so Chesspal puts the badge itself on each move from the start to the
116
149
  * last one the book knows, where the AI gave none; a game from another position has none.
@@ -132,7 +165,7 @@ function bookPliesOfFacts(record, moves) {
132
165
  const firstOut = moves.findIndex((move) => move.outOfBook);
133
166
  return record.tags.FEN !== undefined ? 0 : firstOut >= 0 ? firstOut : moves.length;
134
167
  }
135
- function analysisOf({ record, name }, playerName) {
168
+ function analysisOf({ record, name }, profile) {
136
169
  const { tags } = record;
137
170
  const ai = sideNamed(record, "AI");
138
171
  const control = timeControlOfTag(tags.TimeControl ?? "");
@@ -145,7 +178,8 @@ function analysisOf({ record, name }, playerName) {
145
178
  black: tags.Black ?? "?",
146
179
  ai,
147
180
  level: ai === undefined ? undefined : levelOf(record),
148
- player: ai === undefined && playerName !== undefined ? sideNamed(record, playerName) : undefined,
181
+ // The player by their name, or by their names on the sites (D-93).
182
+ player: ai === undefined ? sideNamed(record, profile.name) ?? accountSide(record.tags, profile.accounts) : undefined,
149
183
  date: knownTag(tags.Date),
150
184
  timeControl: control === undefined ? knownTag(tags.TimeControl) : timeControlText(control),
151
185
  result: ended?.result,
@@ -1,4 +1,5 @@
1
1
  export { analyseGame } from "./analyse.ts";
2
2
  export { annotateGame } from "./annotate.ts";
3
3
  export { deleteReview } from "./delete-review.ts";
4
+ export { type ListedGame, type ListedGames, siteGames, SiteGamesRefused } from "./site-games.ts";
4
5
  export { studyGames } from "./study-games.ts";
@@ -1,5 +1,6 @@
1
- // The commands on a game over or kept to study: the analyst's analysis and review, the games kept and the reviews deleted.
1
+ // The commands on a game over or kept to study: the analyst's analysis and review, the games kept, the latest on the sites, and the reviews deleted.
2
2
  export { analyseGame } from "./analyse.js";
3
3
  export { annotateGame } from "./annotate.js";
4
4
  export { deleteReview } from "./delete-review.js";
5
+ export { siteGames, SiteGamesRefused } from "./site-games.js";
5
6
  export { studyGames } from "./study-games.js";
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { type Side } from "../../core/index.ts";
3
+ import { type RatingSite } from "../../profiles/index.ts";
4
+ import { type SiteGame, type SiteGames } from "../../sites/index.ts";
5
+ import { type Context } from "../registry/index.ts";
6
+ /** A game of the list, with the side the player had when one of their names on the sites is on one side alone (D-93). */
7
+ export interface ListedGame extends SiteGame {
8
+ readonly player?: Side;
9
+ }
10
+ /** The latest games on the sites, with the player's side in each. */
11
+ export interface ListedGames extends Omit<SiteGames, "games"> {
12
+ readonly games: readonly ListedGame[];
13
+ }
14
+ /** Why the list is refused, for the page to say it in the player's words: a game going on, no name on the sites, or no network to ask them. */
15
+ export declare class SiteGamesRefused extends Error {
16
+ readonly refusal: "game-going" | "no-accounts" | "no-network";
17
+ constructor(refusal: SiteGamesRefused["refusal"], message: string);
18
+ }
19
+ /** The latest games on the sites, one line each without their PGN, the latest first; the page gets the PGNs too. */
20
+ export declare const siteGames: import("../registry/registry.ts").Command<z.ZodObject<{}, z.core.$strip>, ListedGames>;
21
+ /**
22
+ * The latest games on the sites of the profile in use, or on one of them alone, each with the player's side; refused when the profile names no account
23
+ * there, or when Chesspal cannot ask the network.
24
+ */
25
+ export declare function profileSiteGames(context: Context, only?: RatingSite): Promise<ListedGames>;
26
+ /** Refused while a game goes on, as every analysis is, with the page's code. */
27
+ export declare function refusedWhilePlaying(context: Context): void;
@@ -0,0 +1,84 @@
1
+ // The player's latest games on Lichess and Chess.com (D-110), from the names in the profile in use, to study one: asked anew each time, never kept.
2
+ import { z } from "zod";
3
+ import { accountSide } from "../../core/index.js";
4
+ import { activeProfile } from "../../profiles/index.js";
5
+ import { readSiteGames } from "../../sites/index.js";
6
+ import { noGameGoingOn } from "../answers/index.js";
7
+ import { siteText } from "../profiles/index.js";
8
+ import { defineCommand } from "../registry/index.js";
9
+ /** Why the list is refused, for the page to say it in the player's words: a game going on, no name on the sites, or no network to ask them. */
10
+ export class SiteGamesRefused extends Error {
11
+ refusal;
12
+ constructor(refusal, message) {
13
+ super(message);
14
+ this.refusal = refusal;
15
+ }
16
+ }
17
+ /** The latest games on the sites, one line each without their PGN, the latest first; the page gets the PGNs too. */
18
+ export const siteGames = defineCommand({
19
+ name: "site_games",
20
+ description: "Lists the player's latest finished games on Lichess and Chess.com, from their names there in the profile, the latest first: the side "
21
+ + "they had and the opponent, with the ratings, how it went, the speed, the day, and the game's name. analyse_game takes that name as game, and "
22
+ + "keeps the game to study. Never while a game goes on. Example: site_games {}.",
23
+ input: z.object({}),
24
+ readOnly: true,
25
+ openWorld: true,
26
+ run: async (input, context) => {
27
+ refusedWhilePlaying(context);
28
+ return profileSiteGames(context);
29
+ },
30
+ format: ({ games, answered, unreached, unknown }) => {
31
+ const none = games.length === 0 && answered.length > 0 ? ["No finished game of standard chess on the sites."] : [];
32
+ const missed = [
33
+ ...(unreached.length === 0 ? [] : [`${sitesText(unreached)} could not be reached: tell the player, and ask again when they ask.`]),
34
+ ...(unknown.length === 0 ? [] : [`${sitesText(unknown)} ${unknown.length === 1 ? "has" : "have"} no player by the profile's name there: the player can correct it in the profile.`]),
35
+ ];
36
+ const next = games.length === 0 ? [] : [`To go over one, analyse_game with game set to its name, such as {"game": "${games[0].name}"}.`];
37
+ return [...games.map(gameLine), ...none, ...missed, ...next].join("\n");
38
+ },
39
+ });
40
+ /**
41
+ * The latest games on the sites of the profile in use, or on one of them alone, each with the player's side; refused when the profile names no account
42
+ * there, or when Chesspal cannot ask the network.
43
+ */
44
+ export async function profileSiteGames(context, only) {
45
+ const { accounts } = activeProfile(context.home());
46
+ const asked = only === undefined ? accounts : only === "lichess" ? { lichess: accounts?.lichess } : { chesscom: accounts?.chesscom };
47
+ if (asked?.lichess === undefined && asked?.chesscom === undefined) {
48
+ throw new SiteGamesRefused("no-accounts", `Tell the player: put your name on ${only === undefined ? "Lichess or Chess.com" : siteText[only]} in your profile, with the pencil beside your name in Who's playing? on the board page. Nothing changed.`);
49
+ }
50
+ if (context.fetch === undefined) {
51
+ throw new SiteGamesRefused("no-network", "Chesspal cannot reach the sites from here. Nothing changed.");
52
+ }
53
+ const read = await readSiteGames(asked, context.fetch);
54
+ return { ...read, games: read.games.map((game) => withPlayer(game, accounts)) };
55
+ }
56
+ /** Refused while a game goes on, as every analysis is, with the page's code. */
57
+ export function refusedWhilePlaying(context) {
58
+ try {
59
+ noGameGoingOn(context);
60
+ }
61
+ catch (error) {
62
+ throw new SiteGamesRefused("game-going", error.message);
63
+ }
64
+ }
65
+ function withPlayer(game, accounts) {
66
+ const player = accountSide({ White: game.white, Black: game.black, Site: game.site }, accounts);
67
+ return player === undefined ? game : { ...game, player };
68
+ }
69
+ /** chess.com/live/186997906366: White (3476) vs BATEK_HA_TPAKTOPE (3102), won, blitz, 2026-11-11; from both sides when the player's is not known. */
70
+ function gameLine(game) {
71
+ const rated = (name, rating) => rating === undefined ? name : `${name} (${rating})`;
72
+ const when = `${game.speed}, ${game.played.slice(0, 10)}.`;
73
+ if (game.player === undefined) {
74
+ return `${game.name}: ${rated(game.white, game.whiteRating)} vs ${rated(game.black, game.blackRating)}, ${game.result}, ${when}`;
75
+ }
76
+ const white = game.player === "white";
77
+ const you = rated(white ? "White" : "Black", white ? game.whiteRating : game.blackRating);
78
+ const opponent = white ? rated(game.black, game.blackRating) : rated(game.white, game.whiteRating);
79
+ const went = game.result === "1/2-1/2" ? "drew" : (game.result === "1-0") === white ? "won" : "lost";
80
+ return `${game.name}: ${you} vs ${opponent}, ${went}, ${when}`;
81
+ }
82
+ function sitesText(sites) {
83
+ return sites.map((site) => siteText[site]).join(" and ");
84
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AnyCommand } from "./registry/index.ts";
2
- export { analyseGame, deleteReview } from "./analysis/index.ts";
2
+ export { analyseGame, deleteReview, siteGames, SiteGamesRefused } from "./analysis/index.ts";
3
3
  export { analystOf, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition } from "./answers/index.ts";
4
4
  export { aiMove, answerDraw, ChatNotClear, DrawRefused, endGame, newGame, offerDraw, startNewGame } from "./game/index.ts";
5
5
  export { chooseDrawnAvatar, deleteGame, editProfile, GameGoingOn, newProfile, ratingsAge, refreshRatings, useProfile } from "./profiles/index.ts";
@@ -1,11 +1,11 @@
1
1
  // The commands: the only way the AI, the command line and the page change a game (D-5, D-27), grouped by area, each defined once.
2
- import { analyseGame, annotateGame, deleteReview, studyGames } from "./analysis/index.js";
2
+ import { analyseGame, annotateGame, deleteReview, siteGames, studyGames } from "./analysis/index.js";
3
3
  import { aiMove, coach, endGame, exportGame, look, newGame, playerMove, ponder, position, status } from "./game/index.js";
4
4
  import { deleteGame, editProfile, newProfile, pastGames, profiles, setAvatar, useProfile } from "./profiles/index.js";
5
5
  import { init, instructions } from "./setup/index.js";
6
6
  import { aiSay, playerSay } from "./talk/index.js";
7
7
  // 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.
8
- export { analyseGame, deleteReview } from "./analysis/index.js";
8
+ export { analyseGame, deleteReview, siteGames, SiteGamesRefused } from "./analysis/index.js";
9
9
  export { analystOf, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition } from "./answers/index.js";
10
10
  export { aiMove, answerDraw, ChatNotClear, DrawRefused, endGame, newGame, offerDraw, startNewGame } from "./game/index.js";
11
11
  export { chooseDrawnAvatar, deleteGame, editProfile, GameGoingOn, newProfile, ratingsAge, refreshRatings, useProfile } from "./profiles/index.js";
@@ -30,6 +30,7 @@ export const commands = [
30
30
  analyseGame,
31
31
  annotateGame,
32
32
  studyGames,
33
+ siteGames,
33
34
  deleteReview,
34
35
  deleteGame,
35
36
  profiles,
@@ -1,4 +1,4 @@
1
1
  export { deleteGame } from "./delete-game.ts";
2
2
  export { pastGameLines, pastGames } from "./past-games.ts";
3
- export { chooseDrawnAvatar, editProfile, GameGoingOn, newProfile, playerLine, profiles, ratingsText, setAvatar, useProfile } from "./profiles.ts";
3
+ export { chooseDrawnAvatar, editProfile, GameGoingOn, newProfile, playerLine, profiles, ratingsText, setAvatar, siteText, useProfile } from "./profiles.ts";
4
4
  export { ratingsAge, refreshRatings } from "./ratings.ts";
@@ -1,5 +1,5 @@
1
1
  // The commands on the players: their profiles, their ratings read from the sites, their past games and the deletion of one.
2
2
  export { deleteGame } from "./delete-game.js";
3
3
  export { pastGameLines, pastGames } from "./past-games.js";
4
- export { chooseDrawnAvatar, editProfile, GameGoingOn, newProfile, playerLine, profiles, ratingsText, setAvatar, useProfile } from "./profiles.js";
4
+ export { chooseDrawnAvatar, editProfile, GameGoingOn, newProfile, playerLine, profiles, ratingsText, setAvatar, siteText, useProfile } from "./profiles.js";
5
5
  export { ratingsAge, refreshRatings } from "./ratings.js";
@@ -1,10 +1,12 @@
1
1
  import { z } from "zod";
2
- import { type DrawnAvatar, type PlayerProfile } from "../../profiles/index.ts";
2
+ import { type DrawnAvatar, type PlayerProfile, type RatingSite } from "../../profiles/index.ts";
3
3
  import { type Context } from "../registry/index.ts";
4
4
  /** A change of profile refused while a game goes on: the page says it in the player's language. */
5
5
  export declare class GameGoingOn extends Error {
6
6
  constructor();
7
7
  }
8
+ /** A site's name as the AI's lines write it. */
9
+ export declare const siteText: Readonly<Record<RatingSite, string>>;
8
10
  /** The profiles, the one in use first. */
9
11
  export declare const profiles: import("../registry/registry.ts").Command<z.ZodObject<{}, z.core.$strip>, {
10
12
  profiles: PlayerProfile[];
@@ -13,6 +13,8 @@ export class GameGoingOn extends Error {
13
13
  super("A game is going on in this profile: finish it, or resign, before changing profile. Nothing changed.");
14
14
  }
15
15
  }
16
+ /** A site's name as the AI's lines write it. */
17
+ export const siteText = { "lichess": "Lichess", "chess.com": "Chess.com" };
16
18
  /** The profiles, the one in use first. */
17
19
  export const profiles = defineCommand({
18
20
  name: "profiles",
@@ -160,7 +162,6 @@ function isPlaying(context) {
160
162
  return current !== undefined && endOf(current.game) === undefined;
161
163
  }
162
164
  const levelText = { learning: "has just learned", casual: "plays now and then", regular: "plays often", tournament: "plays tournaments" };
163
- const siteText = { "lichess": "Lichess", "chess.com": "Chess.com" };
164
165
  function playerText(profile) {
165
166
  const { name, level } = profile;
166
167
  const ratings = ratingsText(profile);
@@ -20,7 +20,10 @@ export interface Context {
20
20
  pageOpen?: () => boolean;
21
21
  /** Opens a folder in the system's file manager: the data folder, from the board page; whether it could; none in the tests. */
22
22
  openFolder?: (folder: string) => Promise<boolean>;
23
- /** Asks the sites the player plays on for their ratings (D-105): only the page and the command line, never a tool of the AI's (D-106); none in the tests. */
23
+ /**
24
+ * Asks the sites the player plays on: their ratings (D-105), only for the page and the command line, never a tool of the AI's (D-106); their latest
25
+ * games, which the tool site_games asks too (D-110); none in the tests.
26
+ */
24
27
  fetch?: typeof globalThis.fetch;
25
28
  /** Opens the New game window on the board pages open now, where the player chooses the game: whether one was open. */
26
29
  askNewGame?: () => boolean;
@@ -12,6 +12,8 @@ export interface Command<Schema extends z.ZodObject = z.ZodObject, Result = unkn
12
12
  destructive?: boolean;
13
13
  /** Calling the command again with the same input changes nothing more. */
14
14
  idempotent?: boolean;
15
+ /** The command may ask a site outside this computer, as site_games asks Lichess and Chess.com (D-110); every other one stays local. */
16
+ openWorld?: true;
15
17
  /** Hosts load the tool at once instead of on demand: for the ones the AI calls at the start of every chat. */
16
18
  alwaysLoad?: boolean;
17
19
  /** Its answer shows the chat the game as it is: the chat may move on it, and on no other (D-71). */
@@ -0,0 +1,11 @@
1
+ import type { Side } from "./game.ts";
2
+ /** The player's names on the sites they play on, from their profile. */
3
+ export interface SiteNames {
4
+ readonly lichess?: string;
5
+ readonly chesscom?: string;
6
+ }
7
+ /**
8
+ * The side a game loaded from elsewhere names by one of the player's names on the sites (D-93): the one of its site when the Site tag says Lichess or
9
+ * chess.com, either otherwise; none when neither side, or both, has one.
10
+ */
11
+ export declare function accountSide(tags: Readonly<Record<string, string | undefined>>, names: SiteNames | undefined): Side | undefined;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The side a game loaded from elsewhere names by one of the player's names on the sites (D-93): the one of its site when the Site tag says Lichess or
3
+ * chess.com, either otherwise; none when neither side, or both, has one.
4
+ */
5
+ export function accountSide(tags, names) {
6
+ const site = (tags.Site ?? "").toLowerCase();
7
+ const known = [
8
+ site.includes("chess.com") ? undefined : names?.lichess,
9
+ site.includes("lichess") ? undefined : names?.chesscom,
10
+ ].filter((name) => name !== undefined).map((name) => name.trim().toLowerCase());
11
+ const white = known.includes(tags.White?.trim().toLowerCase() ?? "");
12
+ const black = known.includes(tags.Black?.trim().toLowerCase() ?? "");
13
+ return white === black ? undefined : white ? "white" : "black";
14
+ }
@@ -1,3 +1,4 @@
1
+ export { accountSide, type SiteNames } from "./accounts.ts";
1
2
  export { boardText, figurinesOf } from "./board-text.ts";
2
3
  export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.ts";
3
4
  export type { Clock, TimeControl } from "./clock.ts";
@@ -1,4 +1,5 @@
1
1
  // The chess core: the game and its rules, the facts of a position, its record, PGN, the openings. It runs in the browser too (D-17, D-18).
2
+ export { accountSide } from "./accounts.js";
2
3
  export { boardText, figurinesOf } from "./board-text.js";
3
4
  export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.js";
4
5
  export { Game, withSanLetters } from "./game.js";
@@ -11,7 +11,7 @@ import { piecesOf } from "../board/squares.js";
11
11
  import { NewGameDialog } from "../dialogs/new-game.js";
12
12
  import { NameButton, ProfileDialog, ProfilesDialog, Wizard } from "../dialogs/profiles.js";
13
13
  import { SettingsPanel } from "../dialogs/settings.js";
14
- import { playerIn, studiedSides } from "../dialogs/studied.js";
14
+ import { othersOf, playerIn, studiedSides } from "../dialogs/studied.js";
15
15
  import { StudyDialog } from "../dialogs/study.js";
16
16
  import { BadgeCounts } from "../game/badges.js";
17
17
  import { stepped } from "../game/browse.js";
@@ -231,7 +231,7 @@ export function App({ start }) {
231
231
  const playedName = game === undefined || game === null ? undefined : anchorName(game.name);
232
232
  useAnchor({ ready: served !== undefined, shown: studied !== undefined ? anchorName(studied.name) : idle ? undefined : playedName, played: playedName, going }, viewing, (plies) => goTo(plies ?? total), async (wanted) => await post("/api/study", { game: wanted, look: true }, text, name) === undefined);
233
233
  // The tab names the game and the move shown, as the cards name its players; between games, Chesspal alone.
234
- const titled = idle || served === undefined ? undefined : studiedSides(live.record.tags, served.profile.name, playedAs, text.sides, served.profile.accounts);
234
+ const titled = idle || served === undefined ? undefined : studiedSides(live.record.tags, served.profile.name, playedAs, text.sides, served.profile.accounts, othersOf(served.profiles, served.profile.id));
235
235
  useTabTitle(titled === undefined ? undefined : tabTitle(titled.white.name, titled.black.name, at === 0 ? undefined : plies[at - 1]));
236
236
  // The header's theme switch picks the other of the two the page shows, leaving the system's.
237
237
  const switchTheme = () => change({ ...settings, theme: document.documentElement.dataset.theme === "dark" ? "light" : "dark" });
@@ -281,7 +281,7 @@ export function App({ start }) {
281
281
  const profile = served.profile;
282
282
  const picture = _jsx(PlayerPicture, { profile: profile });
283
283
  // In analysis the cards name the players of the game, the player's side at the bottom when they played it (D-69).
284
- const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name, playedAs, text.sides, profile.accounts);
284
+ const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name, playedAs, text.sides, profile.accounts, othersOf(served.profiles, profile.id));
285
285
  const player = sides?.bottom ?? (game === null || game.ai === "black" ? "white" : "black");
286
286
  const ai = player === "white" ? "black" : "white";
287
287
  // The Torretta's face on the move shown: the mood the AI gave, while it lasts (D-70); in analysis, of the side the AI played.
@@ -414,7 +414,7 @@ export function App({ start }) {
414
414
  act("/api/new", { ai_colour: colour === "random" ? "random" : colour === "white" ? "black" : "white", level: chosen, ...(control === undefined ? {} : { clock: control }) });
415
415
  };
416
416
  // In analysis the AI's note goes with the other side's card and the talk with the player's, turned round with the board as in a game (D-101, Zoe).
417
- const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place, picture: pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
417
+ const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place, picture: pictureOf(sides[side], profile, served.profiles, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
418
418
  // A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog. One the AI has to go over, with no
419
419
  // chat at the table, opens the chat's window: the analyst works with the chat's model (D-109).
420
420
  const studyGame = async (studied) => {
@@ -427,7 +427,7 @@ export function App({ start }) {
427
427
  setDialog(toReview ? "chat" : undefined);
428
428
  return undefined;
429
429
  };
430
- return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", review: chatToReview, table: table, settings: settings, onChange: change, name: name, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), served.update === undefined || served.table === undefined
430
+ return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", review: chatToReview, table: table, settings: settings, onChange: change, name: name, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), onProfile: () => setDialog("profile"), you: profile.name, accounts: profile.accounts, others: othersOf(served.profiles, profile.id), ai: name, text: text }), served.update === undefined || served.table === undefined
431
431
  ? null
432
432
  : (_jsx(UpdateDialog, { open: dialog === "update", version: served.update.version, current: served.version, installing: served.update.installing === true, installed: served.update.installed === true, going: going, chat: served.table.chat ? (served.table.held ? "held" : "byHand") : undefined, name: name, onClose: () => setDialog(undefined), text: text })), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true
433
433
  ? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) })
@@ -435,7 +435,7 @@ export function App({ start }) {
435
435
  ? _jsx(UpdateNotice, { version: served.update.version, name: name, text: text })
436
436
  : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
437
437
  ? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
438
- : (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined && studied !== undefined
438
+ : (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, served.profiles, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, served.profiles, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined && studied !== undefined
439
439
  ? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
440
440
  setDraft(words);
441
441
  setUnsent(undefined);
@@ -460,9 +460,10 @@ export function App({ start }) {
460
460
  },
461
461
  }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null && replayed.record.moves.length > 0 ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text }), _jsx(PageFoot, { version: served.version, text: text })] })] }) }));
462
462
  }
463
- /** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
464
- function pictureOf({ who }, profile, size, mood) {
465
- return who === "ai" ? _jsx(Torretta, { size: size, mood: mood, bust: true }) : _jsx(PlayerPicture, { profile: who === "player" ? profile : undefined, size: size });
463
+ /** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, another profile's, or the plain face of someone else. */
464
+ function pictureOf({ who, profile: other }, profile, profiles, size, mood) {
465
+ const shown = who === "player" ? profile : profiles.find(({ id }) => id === other);
466
+ return who === "ai" ? _jsx(Torretta, { size: size, mood: mood, bust: true }) : _jsx(PlayerPicture, { profile: shown, size: size });
466
467
  }
467
468
  /**
468
469
  * Plays the sound of each move the game gains while the page watches it, the AI's and the player's, of its end without a move and of a draw offered: