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
@@ -0,0 +1,48 @@
1
+ // The icon on Windows: a shortcut on the desktop and in the Start menu, made by PowerShell, the only tool every Windows has for it. It runs Node through
2
+ // `conhost --headless`, the one way found that opens no console window.
3
+ import { execFile } from "node:child_process";
4
+ import { win32 } from "node:path";
5
+ /** Makes the shortcuts, on the desktop and in the Start menu as Windows names them for this user, or in `places`: their paths. */
6
+ export function windowsIcons(command, image, places) {
7
+ const env = {
8
+ ...process.env,
9
+ CHESSPAL_ICON_FOLDERS: places === undefined ? "" : `${places.desktop};${places.menu}`,
10
+ CHESSPAL_ICON_TARGET: win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "conhost.exe"),
11
+ CHESSPAL_ICON_ARGUMENTS: ["--headless", command.node, ...command.args].map(quoted).join(" "),
12
+ CHESSPAL_ICON_FOLDER: command.folder,
13
+ CHESSPAL_ICON_IMAGE: image,
14
+ };
15
+ return new Promise((done, failed) => {
16
+ execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", shortcuts], { env, windowsHide: true }, (error, stdout, stderr) => {
17
+ if (error === null) {
18
+ done(stdout.split(/\r?\n/).filter((line) => line.trim() !== ""));
19
+ }
20
+ else {
21
+ failed(new Error(`Windows did not make Chesspal's shortcuts: ${stderr.trim() || error.message}`));
22
+ }
23
+ });
24
+ });
25
+ }
26
+ // The values come in the environment, never in the script: a path may hold any character PowerShell reads. Its output in UTF-8, and an error as its
27
+ // message alone, not in the system's language and code page around it.
28
+ const shortcuts = `
29
+ [Console]::OutputEncoding = [Text.Encoding]::UTF8
30
+ trap { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }
31
+ $ErrorActionPreference = 'Stop'
32
+ $shell = New-Object -ComObject WScript.Shell
33
+ $folders = if ($env:CHESSPAL_ICON_FOLDERS) { $env:CHESSPAL_ICON_FOLDERS -split ';' } else { [Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('Programs') }
34
+ foreach ($folder in $folders) {
35
+ $link = $shell.CreateShortcut((Join-Path $folder 'Chesspal.lnk'))
36
+ $link.TargetPath = $env:CHESSPAL_ICON_TARGET
37
+ $link.Arguments = $env:CHESSPAL_ICON_ARGUMENTS
38
+ $link.WorkingDirectory = $env:CHESSPAL_ICON_FOLDER
39
+ $link.IconLocation = $env:CHESSPAL_ICON_IMAGE
40
+ $link.Description = 'Chesspal'
41
+ $link.Save()
42
+ $link.FullName
43
+ }
44
+ `;
45
+ // An argument as Windows reads it from a command line: in quotes when it has a space; a Windows path never holds a quote.
46
+ function quoted(argument) {
47
+ return /\s/.test(argument) ? `"${argument}"` : argument;
48
+ }
@@ -0,0 +1,6 @@
1
+ export { appBrowser, openedAddress } from "./browser.ts";
2
+ export { hostLaunch, isHostName, playHost, terminalChats } from "./hosts/index.ts";
3
+ export { iconAsked, type IconCommand, iconCommand, type IconPlaces, iconToRemake, makeIcons, noteIconAsked, noteIconMade } from "./icon/index.ts";
4
+ export { noteOpenFailed, openTable, processLauncher, type TableLauncher } from "./launch.ts";
5
+ export { attachTable, tableToJoin } from "./link.ts";
6
+ export { isOlder, startTable, type Table, type TableTimes, tableTimes } from "./table.ts";
@@ -0,0 +1,7 @@
1
+ // The table (D-104): the board page in a process of its own, started by `chesspal` alone, with no chat. Above server/page, which serves the page in either process.
2
+ export { appBrowser, openedAddress } from "./browser.js";
3
+ export { hostLaunch, isHostName, playHost, terminalChats } from "./hosts/index.js";
4
+ export { iconAsked, iconCommand, iconToRemake, makeIcons, noteIconAsked, noteIconMade } from "./icon/index.js";
5
+ export { noteOpenFailed, openTable, processLauncher } from "./launch.js";
6
+ export { attachTable, tableToJoin } from "./link.js";
7
+ export { isOlder, startTable, tableTimes } from "./table.js";
@@ -0,0 +1,19 @@
1
+ import { type Context } from "../../commands/index.ts";
2
+ /** What opening the table does outside this process: start the table apart, open the browser, wait. The tests give their own. */
3
+ export interface TableLauncher {
4
+ /** Starts `chesspal table` apart from this process, which it outlives, in the data folder. */
5
+ start(home: string): void;
6
+ /** Opens the page in the browser: whether it could. */
7
+ open(url: string): Promise<boolean>;
8
+ wait(milliseconds: number): Promise<void>;
9
+ }
10
+ /**
11
+ * Opens the table's page in the browser, the table started first when none answers, or when the one that does is of another version, which is asked to
12
+ * quit; a table of this version another `chesspal` started meanwhile is as good. What to tell the player, in their language; a table that does not start
13
+ * within ten seconds throws.
14
+ */
15
+ export declare function openTable(context: Context, launcher?: TableLauncher): Promise<string>;
16
+ /** Notes why the icon could not open the table, whose console nobody sees, in open.log of the table's folder: the last of them. */
17
+ export declare function noteOpenFailed(context: Context, message: string): void;
18
+ /** The launcher of a real `chesspal`: the table started with the same Node and script, the page opened in the default browser. */
19
+ export declare function processLauncher(platform: string, command?: string[]): TableLauncher;
@@ -0,0 +1,78 @@
1
+ // `chesspal` alone (D-104): the table's page opened in the browser, the table started first, apart from the terminal, when none of this version answers.
2
+ import { spawn } from "node:child_process";
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { styleText } from "node:util";
6
+ import { speaksItalian } from "../../commands/index.js";
7
+ import { tableFolder } from "../../home/index.js";
8
+ import { version } from "../../version.js";
9
+ import { openInBrowser } from "./browser.js";
10
+ import { askToQuit, tableAt } from "./table.js";
11
+ /**
12
+ * Opens the table's page in the browser, the table started first when none answers, or when the one that does is of another version, which is asked to
13
+ * quit; a table of this version another `chesspal` started meanwhile is as good. What to tell the player, in their language; a table that does not start
14
+ * within ten seconds throws.
15
+ */
16
+ export async function openTable(context, launcher = processLauncher(context.platform)) {
17
+ const home = context.home();
18
+ const folder = tableFolder(home);
19
+ const words = speaksItalian(context.language) ? italian : english;
20
+ const deadline = context.now().getTime() + startWait;
21
+ const asked = new Set();
22
+ let started = false;
23
+ for (;;) {
24
+ const table = await tableAt(folder);
25
+ if (table?.version === version) {
26
+ return styleText(["bold", "green"], `♜ ${(await launcher.open(table.url)) ? words.opened(table.url) : words.notOpened(table.url)}`);
27
+ }
28
+ if (table !== undefined && !asked.has(table.port)) {
29
+ asked.add(table.port);
30
+ await askToQuit(table.port);
31
+ }
32
+ else if (table === undefined && !started) {
33
+ started = true;
34
+ launcher.start(home);
35
+ }
36
+ if (context.now().getTime() >= deadline) {
37
+ throw new Error(table === undefined ? words.notStarted : words.notQuit(table.url));
38
+ }
39
+ await launcher.wait(every);
40
+ }
41
+ }
42
+ /** Notes why the icon could not open the table, whose console nobody sees, in open.log of the table's folder: the last of them. */
43
+ export function noteOpenFailed(context, message) {
44
+ try {
45
+ const folder = tableFolder(context.home());
46
+ mkdirSync(folder, { recursive: true });
47
+ writeFileSync(join(folder, "open.log"), `${context.now().toISOString()} ${message}\n`);
48
+ }
49
+ catch {
50
+ // A data folder that cannot be written has no other place for it.
51
+ }
52
+ }
53
+ /** The launcher of a real `chesspal`: the table started with the same Node and script, the page opened in the default browser. */
54
+ export function processLauncher(platform, command = [...process.execArgv, process.argv[1] ?? ""]) {
55
+ return {
56
+ start: (home) => {
57
+ // Its own process group, with no console and nothing of this one's: the terminal can close, the table stays.
58
+ spawn(process.execPath, [...command, "table"], { cwd: home, detached: true, stdio: "ignore", windowsHide: true }).unref();
59
+ },
60
+ open: (url) => openInBrowser(url, platform),
61
+ wait: (milliseconds) => new Promise((done) => setTimeout(done, milliseconds)),
62
+ };
63
+ }
64
+ // A table answers within ten seconds of its start, an older one quitting first, looked for every tenth of a second.
65
+ const startWait = 10_000;
66
+ const every = 100;
67
+ const english = {
68
+ opened: (url) => `Chesspal's table is open: ${url}`,
69
+ notOpened: (url) => `Chesspal's table is at ${url}: open it in your browser.`,
70
+ notStarted: "Chesspal's table did not start within ten seconds: run `chesspal table` to see why.",
71
+ notQuit: (url) => `The table of another Chesspal version, at ${url}, did not close: close its pages in the browser and wait five minutes, then run chesspal again.`,
72
+ };
73
+ const italian = {
74
+ opened: (url) => `Il tavolo di Chesspal è aperto: ${url}`,
75
+ notOpened: (url) => `Il tavolo di Chesspal è su ${url}: aprilo nel browser.`,
76
+ notStarted: "Il tavolo di Chesspal non è partito in dieci secondi: lancia `chesspal table` per vedere perché.",
77
+ notQuit: (url) => `Il tavolo di un'altra versione di Chesspal, su ${url}, non si è chiuso: chiudi le sue pagine nel browser, aspetta cinque minuti e lancia di nuovo chesspal.`,
78
+ };
@@ -0,0 +1,10 @@
1
+ import type { Context } from "../../commands/index.ts";
2
+ import type { Page } from "../page/page.ts";
3
+ /**
4
+ * Attaches the chat to the table of its data folder, when one of its version answers and no other chat is at it: the page the chat then uses, whose `lost`
5
+ * is told once the table is gone or no longer knows the chat. An older table is asked to quit; with none, a newer one or another chat at it, none, and
6
+ * the chat serves its own page.
7
+ */
8
+ export declare function attachTable(context: Context, lost: () => void): Promise<Page | undefined>;
9
+ /** Whether a table of this version answers for the chat's data folder with no chat at it: a chat that serves its own page goes there at its next tool. */
10
+ export declare function tableToJoin(context: Context): Promise<boolean>;
@@ -0,0 +1,230 @@
1
+ // A chat attached to the table (D-104): the board page is the table's, and the chat tells it over HTTP, on this machine, what a page it served itself would
2
+ // read from it: that it calls a tool, a copy of what it saw after each, and that the New game window should open.
3
+ import { Agent, request } from "node:http";
4
+ import { aiHostOf, aiModelOf } from "../../games/index.js";
5
+ import { tableFolder } from "../../home/index.js";
6
+ import { outdatedHostFiles } from "../../host-files/index.js";
7
+ import { gameFolder } from "../../profiles/index.js";
8
+ import { version } from "../../version.js";
9
+ import { jsonOf } from "../page/http.js";
10
+ import { askToQuit, isOlder, tableAt } from "./table.js";
11
+ /**
12
+ * Attaches the chat to the table of its data folder, when one of its version answers and no other chat is at it: the page the chat then uses, whose `lost`
13
+ * is told once the table is gone or no longer knows the chat. An older table is asked to quit; with none, a newer one or another chat at it, none, and
14
+ * the chat serves its own page.
15
+ */
16
+ export async function attachTable(context, lost) {
17
+ const table = await tableOf(context);
18
+ if (table === undefined) {
19
+ return undefined;
20
+ }
21
+ if (table.version !== version) {
22
+ if (table.version === undefined || isOlder(table.version, version)) {
23
+ await askToQuit(table.port);
24
+ }
25
+ return undefined;
26
+ }
27
+ const link = new TableLink(table.port, context, lost);
28
+ return (await link.attach()) ? tablePage(link, table.url) : undefined;
29
+ }
30
+ /** Whether a table of this version answers for the chat's data folder with no chat at it: a chat that serves its own page goes there at its next tool. */
31
+ export async function tableToJoin(context) {
32
+ const table = await tableOf(context);
33
+ return table !== undefined && table.version === version && table.chat === undefined;
34
+ }
35
+ /** The table of the chat's data folder, when one answers; none while the data folder cannot be made. */
36
+ async function tableOf(context) {
37
+ try {
38
+ return await tableAt(tableFolder(context.home()));
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ // How long the table may take to answer the chat: it answers in a millisecond, so one that takes more than a second is stuck (Federico).
45
+ const answerWait = 1_500;
46
+ /** The page as the attached chat uses it: what a page of its own reads from the chat, the chat sends the table. */
47
+ function tablePage(link, url) {
48
+ return {
49
+ url,
50
+ heard: () => link.copy(true),
51
+ chatCalls: () => link.calls(),
52
+ toolEnded: () => link.copy(false),
53
+ open: () => link.open,
54
+ inUse: () => true,
55
+ askNewGame: () => link.askNewGame(),
56
+ close: async () => link.leave(),
57
+ };
58
+ }
59
+ /** The chat's end of its place at the table: its token, whether a browser has the page open, and the copies it sends, in order, one at a time. */
60
+ class TableLink {
61
+ /** Whether a browser had the page open at the table's last answer. */
62
+ open = false;
63
+ token = "";
64
+ attachment;
65
+ agent = new Agent({ keepAlive: true, maxSockets: 1 });
66
+ news = false;
67
+ copying = false;
68
+ left = false;
69
+ port;
70
+ context;
71
+ lost;
72
+ constructor(port, context, lost) {
73
+ this.port = port;
74
+ this.context = context;
75
+ this.lost = lost;
76
+ }
77
+ /** Attaches: whether the table took the chat. The answer then stays open for as long as both live; when it closes, the chat has lost the table. */
78
+ attach() {
79
+ return new Promise((done) => {
80
+ let text = "";
81
+ let attached = false;
82
+ // A table that takes the chat and then says nothing is no table: the chat serves its own page (Priya).
83
+ const silent = setTimeout(() => this.attachment?.destroy(), answerWait);
84
+ silent.unref();
85
+ this.attachment = request({ ...this.address("/api/chat"), agent: false }, (response) => {
86
+ response.setEncoding("utf8");
87
+ // The first line gives the chat its token; a table that refuses it answers with a status of its own and closes.
88
+ response.on("data", (chunk) => {
89
+ if (attached) {
90
+ return;
91
+ }
92
+ text += chunk;
93
+ const line = text.indexOf("\n");
94
+ if (line === -1) {
95
+ return;
96
+ }
97
+ const answer = jsonOf(text.slice(0, line));
98
+ clearTimeout(silent);
99
+ if (response.statusCode === 200 && typeof answer?.token === "string") {
100
+ attached = true;
101
+ this.token = answer.token;
102
+ this.open = answer.open === true;
103
+ }
104
+ else {
105
+ this.attachment?.destroy();
106
+ }
107
+ done(attached);
108
+ });
109
+ response.on("close", () => {
110
+ clearTimeout(silent);
111
+ return attached ? this.leave() : done(false);
112
+ });
113
+ });
114
+ // The answer kept open never keeps the chat's process alive: it ends when its host goes.
115
+ this.attachment.on("socket", (socket) => socket.unref());
116
+ this.attachment.on("error", () => {
117
+ clearTimeout(silent);
118
+ done(false);
119
+ });
120
+ this.attachment.end(JSON.stringify(attachment(this.context)));
121
+ });
122
+ }
123
+ /** The chat calls a tool: the table hears it before the tool runs, which starts the AI's clock when nobody had heard the player. */
124
+ async calls() {
125
+ this.answered(await this.post("/api/chat/call", {}));
126
+ }
127
+ /** Sends a copy of what the chat saw once the call under way has ended, those asked meanwhile as one; with `news`, the pages hear of it. */
128
+ copy(news) {
129
+ this.news ||= news;
130
+ if (this.copying) {
131
+ return;
132
+ }
133
+ this.copying = true;
134
+ setImmediate(() => {
135
+ this.copying = false;
136
+ const body = { ...seenBy(this.context), news: this.news };
137
+ this.news = false;
138
+ void this.post("/api/chat/read", body).then((answer) => this.answered(answer));
139
+ });
140
+ }
141
+ /** Asks the pages open to show the New game window, without waiting: whether one was open, as the table last said. */
142
+ askNewGame() {
143
+ void this.post("/api/chat/new-game", {}).then((answer) => this.answered(answer));
144
+ return this.open;
145
+ }
146
+ /** Leaves the table, once: the chat's process ends, the table is gone, or no longer knows the chat. */
147
+ leave() {
148
+ if (!this.left) {
149
+ this.left = true;
150
+ this.attachment?.destroy();
151
+ this.agent.destroy();
152
+ this.lost();
153
+ }
154
+ }
155
+ // The table did not answer, or no longer knows the chat: it is lost. A request it failed alone changes nothing.
156
+ answered(answer) {
157
+ if (answer === "lost") {
158
+ this.leave();
159
+ }
160
+ else if (typeof answer?.open === "boolean") {
161
+ this.open = answer.open;
162
+ }
163
+ }
164
+ // A request to the table on the chat's own connection, kept alive, and once more on a new one when the table had just closed the one kept.
165
+ post(path, body, retry = true) {
166
+ if (this.left) {
167
+ return Promise.resolve("lost");
168
+ }
169
+ return new Promise((done) => {
170
+ const asked = request({ ...this.address(path), agent: this.agent }, (response) => {
171
+ const chunks = [];
172
+ response.on("data", (chunk) => chunks.push(chunk));
173
+ response.on("end", () => done(answerOf(response.statusCode, Buffer.concat(chunks).toString("utf8"))));
174
+ response.on("close", () => done("lost"));
175
+ });
176
+ // A table that stops answering would hold every tool of the chat's: it is lost, and the chat serves its own page (Priya).
177
+ asked.setTimeout(answerWait, () => asked.destroy(new Error("The table did not answer.")));
178
+ asked.on("error", (error) => {
179
+ // The table closes a connection idle for 5 s as Node reuses it: the request never reached it (Federico).
180
+ if (retry && asked.reusedSocket && error.code === "ECONNRESET") {
181
+ void this.post(path, body, false).then(done);
182
+ }
183
+ else {
184
+ done("lost");
185
+ }
186
+ });
187
+ asked.end(JSON.stringify({ ...body, token: this.token }));
188
+ });
189
+ }
190
+ address(path) {
191
+ return { host: "127.0.0.1", port: this.port, path, method: "POST", headers: { "Content-Type": "application/json" } };
192
+ }
193
+ }
194
+ /** A table that no longer knows the chat answers 409; another status is one request it failed, and the chat stays. */
195
+ function answerOf(status, text) {
196
+ if (status === 409) {
197
+ return "lost";
198
+ }
199
+ return status === 200 ? jsonOf(text) : undefined;
200
+ }
201
+ /** What the chat says of itself as it attaches:its version, session and folder, its host, and whether its folder needs init again. */
202
+ function attachment(context) {
203
+ return {
204
+ version,
205
+ session: process.env.CLAUDE_CODE_SESSION_ID ?? null,
206
+ folder: context.folder,
207
+ host: context.host?.(),
208
+ outdated: outdatedHostFiles(context.folder, context.platform).length > 0,
209
+ };
210
+ }
211
+ /** What the chat saw, for the table's copy: the game and the position, the words it was given, its host, and who played as the AI (D-82, D-97). */
212
+ function seenBy(context) {
213
+ const folder = gameFolderOf(context);
214
+ return {
215
+ seen: context.seen,
216
+ positionSeen: context.positionSeen,
217
+ given: [...context.given],
218
+ host: context.host?.(),
219
+ aiModel: folder === undefined ? undefined : aiModelOf(folder),
220
+ aiHost: folder === undefined ? undefined : aiHostOf(folder),
221
+ };
222
+ }
223
+ function gameFolderOf(context) {
224
+ try {
225
+ return gameFolder(context.home());
226
+ }
227
+ catch {
228
+ return undefined;
229
+ }
230
+ }
@@ -0,0 +1,35 @@
1
+ import type { Context } from "../../commands/index.ts";
2
+ import type { TableChats } from "../page/table.ts";
3
+ /** A table served: its address, and how to close it. */
4
+ export interface Table {
5
+ readonly url: string;
6
+ /** Settled once the table has closed: by itself, asked by another Chesspal version, or by close. */
7
+ readonly closed: Promise<void>;
8
+ close(): Promise<void>;
9
+ }
10
+ /** How long the table stays unused before it closes, and how often it looks, in milliseconds. */
11
+ export interface TableTimes {
12
+ readonly idle: number;
13
+ readonly every: number;
14
+ }
15
+ /** Five minutes with no browser and no chat, looked at every fifteen seconds (D-104). */
16
+ export declare const tableTimes: TableTimes;
17
+ /** A table that answers on its ports: its address, port and version, and the folder of the chat attached to it, when one is. */
18
+ export interface TableFound {
19
+ readonly url: string;
20
+ readonly port: number;
21
+ readonly version: string | undefined;
22
+ readonly chat: string | undefined;
23
+ }
24
+ /**
25
+ * Starts the table on the context's data folder, its folder set up as init does, its page opening chats through `chats`: undefined when another table
26
+ * started at the same time took a lower port, since a table's ports are its lock. It closes once no page has been open, no chat attached and no request
27
+ * come, for `times.idle`.
28
+ */
29
+ export declare function startTable(context: Context, times?: TableTimes, chats?: TableChats): Promise<Table | undefined>;
30
+ /** The table of a folder that answers on its ports, the lowest of them; its ports are asked together. */
31
+ export declare function tableAt(folder: string): Promise<TableFound | undefined>;
32
+ /** Asks the table at a port to quit; it answers before it goes, and a table that cannot be reached, or does not answer within a second or so, is let be. */
33
+ export declare function askToQuit(port: number): Promise<void>;
34
+ /** Whether a version is older than another, part by part: 0.2.4 is older than 0.10.0. */
35
+ export declare function isOlder(one: string, other: string): boolean;
@@ -0,0 +1,94 @@
1
+ // The table (D-104): the board page served in a process of its own, on a folder of the data folder that is one for every profile, which a chat attaches
2
+ // to; it closes by itself once no browser has had the page open, and no chat been attached, for a while.
3
+ import { request } from "node:http";
4
+ import { tableFolder } from "../../home/index.js";
5
+ import { setUpHosts } from "../../host-files/index.js";
6
+ import { pageAt, pagePorts, samePath } from "../bell.js";
7
+ import { startPage } from "../page/page.js";
8
+ /** Five minutes with no browser and no chat, looked at every fifteen seconds (D-104). */
9
+ export const tableTimes = { idle: 5 * 60_000, every: 15_000 };
10
+ /**
11
+ * Starts the table on the context's data folder, its folder set up as init does, its page opening chats through `chats`: undefined when another table
12
+ * started at the same time took a lower port, since a table's ports are its lock. It closes once no page has been open, no chat attached and no request
13
+ * come, for `times.idle`.
14
+ */
15
+ export async function startTable(context, times = tableTimes, chats) {
16
+ const folder = tableFolder(context.home());
17
+ setUpHosts(folder, context.platform);
18
+ let quit = () => undefined;
19
+ const quitting = new Promise((done) => {
20
+ quit = done;
21
+ });
22
+ let lastUsed = context.now().getTime();
23
+ const used = () => {
24
+ lastUsed = context.now().getTime();
25
+ };
26
+ const page = await startPage({ ...context, folder }, { table: { used, quit, chats } });
27
+ if (await lostTo(page.url, folder)) {
28
+ await page.close();
29
+ return undefined;
30
+ }
31
+ const watch = closeWhenIdle(page, () => context.now().getTime() - lastUsed, used, times, quit);
32
+ const closed = quitting.then(async () => {
33
+ clearInterval(watch);
34
+ await page.close();
35
+ });
36
+ return {
37
+ url: page.url,
38
+ closed,
39
+ close: () => {
40
+ quit();
41
+ return closed;
42
+ },
43
+ };
44
+ }
45
+ /** The table of a folder that answers on its ports, the lowest of them; its ports are asked together. */
46
+ export async function tableAt(folder) {
47
+ const ports = pagePorts(folder);
48
+ const said = await Promise.all(ports.map(pageAt));
49
+ const index = said.findIndex((one) => one?.table === true && samePath(one.folder, folder));
50
+ const port = ports[index];
51
+ return port === undefined ? undefined : { url: `http://127.0.0.1:${port}/`, port, version: said[index]?.version, chat: said[index]?.chat };
52
+ }
53
+ /** Asks the table at a port to quit; it answers before it goes, and a table that cannot be reached, or does not answer within a second or so, is let be. */
54
+ export function askToQuit(port) {
55
+ return new Promise((done) => {
56
+ const asked = request({ host: "127.0.0.1", port, path: "/api/table/quit", method: "POST", headers: { "Content-Type": "application/json" }, agent: false }, (response) => {
57
+ response.resume();
58
+ response.on("end", () => done());
59
+ });
60
+ asked.setTimeout(quitWait, () => asked.destroy());
61
+ asked.on("error", () => done());
62
+ asked.end("{}");
63
+ });
64
+ }
65
+ // A table answers at once: one stuck must not hold the chat's first tool, nor `chesspal`.
66
+ const quitWait = 1_500;
67
+ /** Whether a version is older than another, part by part: 0.2.4 is older than 0.10.0. */
68
+ export function isOlder(one, other) {
69
+ const parts = (version) => version.split(/[.+-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
70
+ const [mine, theirs] = [parts(one), parts(other)];
71
+ const differs = mine.findIndex((part, index) => part !== theirs[index]);
72
+ return differs !== -1 && mine[differs] < theirs[differs];
73
+ }
74
+ /** Whether a table of the same folder answers on a lower port than this one's: the other won, and this one goes. */
75
+ async function lostTo(url, folder) {
76
+ const port = Number(new URL(url).port);
77
+ const lower = pagePorts(folder).filter((other) => other < port);
78
+ const found = await Promise.all(lower.map(pageAt));
79
+ return found.some((said) => said?.table === true && samePath(said.folder, folder));
80
+ }
81
+ /**
82
+ * Looks every `times.every` whether a browser has the page open or a chat is attached, which uses the table, and quits once it has been unused for
83
+ * `times.idle`: the page that `chesspal` opens counts from its first request. The timer keeps the process alive till then.
84
+ */
85
+ function closeWhenIdle(page, unusedFor, used, times, quit) {
86
+ return setInterval(() => {
87
+ if (page.inUse()) {
88
+ used();
89
+ }
90
+ else if (unusedFor() >= times.idle) {
91
+ quit();
92
+ }
93
+ }, times.every);
94
+ }
@@ -0,0 +1,4 @@
1
+ import type { SiteReading } from "../profiles/index.ts";
2
+ import { type Fetch } from "./request.ts";
3
+ /** What Chess.com says of a name: undefined when it could not be reached. */
4
+ export declare function chesscomReading(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteReading | undefined>;
@@ -0,0 +1,25 @@
1
+ import { counted } from "./choice.js";
2
+ import { askSite } from "./request.js";
3
+ /** What Chess.com says of a name: undefined when it could not be reached. */
4
+ export async function chesscomReading(account, fetch, signal) {
5
+ const statistics = await askSite(`https://api.chess.com/pub/player/${encodeURIComponent(account.toLowerCase())}/stats`, fetch, signal);
6
+ if (statistics === undefined || (statistics.status !== 200 && statistics.status !== 404)) {
7
+ return undefined;
8
+ }
9
+ if (statistics.status === 404) {
10
+ return { account, found: false };
11
+ }
12
+ const speeds = statistics.body;
13
+ const chosen = counted((speed) => {
14
+ const last = speeds?.[`chess_${speed}`]?.last;
15
+ return typeof last?.rating === "number" && typeof last.rd === "number"
16
+ ? { value: last.rating, deviation: last.rd, established: last.rd < provisional, ...(typeof last.date === "number" ? { played: dayOf(last.date) } : {}) }
17
+ : undefined;
18
+ });
19
+ return chosen === undefined ? { account, found: true } : { account, found: true, rating: { ...chosen, site: "chess.com" } };
20
+ }
21
+ // Chess.com gives no provisional mark: a rating is provisional while its deviation (RD) is as wide as Lichess's provisional ones.
22
+ const provisional = 110;
23
+ function dayOf(seconds) {
24
+ return new Date(seconds * 1000).toISOString().slice(0, 10);
25
+ }
@@ -0,0 +1,17 @@
1
+ import { type Speed } from "../profiles/index.ts";
2
+ /** A site's rating at one speed, as it gives it: established once it is no longer provisional. */
3
+ export interface Candidate {
4
+ readonly value: number;
5
+ readonly deviation: number;
6
+ readonly established: boolean;
7
+ readonly played?: string;
8
+ }
9
+ /** The rating that counts, with its speed. */
10
+ export interface Played {
11
+ readonly speed: Speed;
12
+ readonly value: number;
13
+ readonly deviation: number;
14
+ readonly played?: string;
15
+ }
16
+ /** The first established rating in the order the speeds count: none when every one is missing or provisional. */
17
+ export declare function counted(at: (speed: Speed) => Candidate | undefined): Played | undefined;
@@ -0,0 +1,13 @@
1
+ // Which of a site's ratings counts (D-106): the rapid, and when it is missing or provisional the classical, then the blitz.
2
+ import { speeds } from "../profiles/index.js";
3
+ /** The first established rating in the order the speeds count: none when every one is missing or provisional. */
4
+ export function counted(at) {
5
+ for (const speed of speeds) {
6
+ const candidate = at(speed);
7
+ if (candidate?.established === true) {
8
+ const { value, deviation, played } = candidate;
9
+ return { speed, value: Math.round(value), deviation: Math.round(deviation), ...(played === undefined ? {} : { played }) };
10
+ }
11
+ }
12
+ return undefined;
13
+ }
@@ -0,0 +1,2 @@
1
+ export { type Fetch } from "./request.ts";
2
+ export { readingsStale, readSites } from "./sites.ts";
@@ -0,0 +1,4 @@
1
+ // The sites the player plays on (D-24, D-105): their public ratings on Lichess and chess.com, read from the names in their profile, without a key. Below
2
+ // the commands, beside the profiles, whose types it fills; never asked by a tool of the AI's (D-106).
3
+ export {} from "./request.js";
4
+ export { readingsStale, readSites } from "./sites.js";
@@ -0,0 +1,4 @@
1
+ import type { SiteReading } from "../profiles/index.ts";
2
+ import { type Fetch } from "./request.ts";
3
+ /** What Lichess says of a name: undefined when it could not be reached. */
4
+ export declare function lichessReading(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteReading | undefined>;
@@ -0,0 +1,32 @@
1
+ import { counted } from "./choice.js";
2
+ import { askSite } from "./request.js";
3
+ /** What Lichess says of a name: undefined when it could not be reached. */
4
+ export async function lichessReading(account, fetch, signal) {
5
+ const user = await askSite(`${address}/${encodeURIComponent(account)}`, fetch, signal);
6
+ if (user === undefined || (user.status !== 200 && user.status !== 404)) {
7
+ return undefined;
8
+ }
9
+ const profile = user.body;
10
+ // A closed account is no player any more.
11
+ if (user.status === 404 || profile?.disabled === true) {
12
+ return { account, found: false };
13
+ }
14
+ const chosen = counted((speed) => {
15
+ const speedRating = profile?.perfs?.[speed];
16
+ return typeof speedRating?.rating === "number" && typeof speedRating.rd === "number" && typeof speedRating.games === "number"
17
+ ? { value: speedRating.rating, deviation: speedRating.rd, established: speedRating.prov !== true && speedRating.games > 0 }
18
+ : undefined;
19
+ });
20
+ if (chosen === undefined) {
21
+ return { account, found: true };
22
+ }
23
+ const played = await lastPlayed(account, chosen.speed, fetch, signal);
24
+ return { account, found: true, rating: { ...chosen, site: "lichess", ...(played === undefined ? {} : { played }) } };
25
+ }
26
+ const address = "https://lichess.org/api/user";
27
+ // The day of the last game at that speed, from the speed's statistics: the profile gives only when the player was last seen.
28
+ async function lastPlayed(account, speed, fetch, signal) {
29
+ const statistics = await askSite(`${address}/${encodeURIComponent(account)}/perf/${speed}`, fetch, signal);
30
+ const last = statistics?.body?.stat?.playStreak?.lastDate;
31
+ return typeof last === "string" && /^\d{4}-\d{2}-\d{2}/.test(last) ? last.slice(0, 10) : undefined;
32
+ }