chesspal 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -2
- package/dist/cli/chat-hook.d.ts +2 -0
- package/dist/cli/chat-hook.js +33 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +28 -0
- package/dist/cli/command-line.d.ts +7 -0
- package/dist/cli/command-line.js +177 -0
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +28 -0
- package/dist/commands/ai-move.js +85 -0
- package/dist/commands/ai-say.d.ts +22 -0
- package/dist/commands/ai-say.js +58 -0
- package/dist/commands/analyse-game.d.ts +65 -0
- package/dist/commands/analyse-game.js +265 -0
- package/dist/commands/annotate-game.d.ts +39 -0
- package/dist/commands/annotate-game.js +131 -0
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +67 -0
- package/dist/commands/context.js +37 -0
- package/dist/commands/delete-game.d.ts +7 -0
- package/dist/commands/delete-game.js +30 -0
- package/dist/commands/delete-review.d.ts +8 -0
- package/dist/commands/delete-review.js +34 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +13 -0
- package/dist/commands/drawings.js +25 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +67 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +27 -0
- package/dist/commands/game-summary.d.ts +78 -0
- package/dist/commands/game-summary.js +102 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.js +64 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -0
- package/dist/commands/levels.d.ts +9 -0
- package/dist/commands/levels.js +25 -0
- package/dist/commands/look.d.ts +31 -0
- package/dist/commands/look.js +64 -0
- package/dist/commands/new-game.d.ts +68 -0
- package/dist/commands/new-game.js +218 -0
- package/dist/commands/opening-book.d.ts +13 -0
- package/dist/commands/opening-book.js +84 -0
- package/dist/commands/page-link.d.ts +11 -0
- package/dist/commands/page-link.js +28 -0
- package/dist/commands/past-games.d.ts +11 -0
- package/dist/commands/past-games.js +47 -0
- package/dist/commands/play-move.d.ts +37 -0
- package/dist/commands/play-move.js +94 -0
- package/dist/commands/player-move.d.ts +13 -0
- package/dist/commands/player-move.js +51 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +52 -0
- package/dist/commands/ponder.js +179 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +130 -0
- package/dist/commands/profiles.d.ts +69 -0
- package/dist/commands/profiles.js +163 -0
- package/dist/commands/registry.d.ts +59 -0
- package/dist/commands/registry.js +62 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +35 -0
- package/dist/commands/status.js +128 -0
- package/dist/commands/study-games.d.ts +5 -0
- package/dist/commands/study-games.js +40 -0
- package/dist/commands/study.d.ts +38 -0
- package/dist/commands/study.js +106 -0
- package/dist/commands/view.d.ts +14 -0
- package/dist/commands/view.js +125 -0
- package/dist/core/board-text.d.ts +7 -0
- package/dist/core/board-text.js +18 -0
- package/dist/core/book.json +1 -0
- package/dist/core/clock.d.ts +45 -0
- package/dist/core/clock.js +109 -0
- package/dist/core/facts.d.ts +55 -0
- package/dist/core/facts.js +175 -0
- package/dist/core/game.d.ts +95 -0
- package/dist/core/game.js +352 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +12 -0
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -0
- package/dist/core/levels.d.ts +20 -0
- package/dist/core/levels.js +33 -0
- package/dist/core/material.d.ts +9 -0
- package/dist/core/material.js +12 -0
- package/dist/core/moods.d.ts +21 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +35 -0
- package/dist/core/openings.js +98 -0
- package/dist/core/openings.json +1 -0
- package/dist/core/pgn.d.ts +10 -0
- package/dist/core/pgn.js +213 -0
- package/dist/core/profile.d.ts +117 -0
- package/dist/core/profile.js +232 -0
- package/dist/core/record.d.ts +21 -0
- package/dist/core/record.js +25 -0
- package/dist/core/review.d.ts +80 -0
- package/dist/core/review.js +146 -0
- package/dist/games/ai-model.d.ts +5 -0
- package/dist/games/ai-model.js +15 -0
- package/dist/games/analysis.d.ts +12 -0
- package/dist/games/analysis.js +26 -0
- package/dist/games/archive.d.ts +63 -0
- package/dist/games/archive.js +242 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +56 -0
- package/dist/games/current-game.js +134 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +53 -0
- package/dist/games/index.d.ts +11 -0
- package/dist/games/index.js +12 -0
- package/dist/games/plans.d.ts +9 -0
- package/dist/games/plans.js +23 -0
- package/dist/games/table-talk.d.ts +12 -0
- package/dist/games/table-talk.js +25 -0
- package/dist/games/talk.d.ts +36 -0
- package/dist/games/talk.js +131 -0
- package/dist/games/thoughts.d.ts +15 -0
- package/dist/games/thoughts.js +60 -0
- package/dist/home/data-folder.d.ts +12 -0
- package/dist/home/data-folder.js +55 -0
- package/dist/home/index.d.ts +2 -0
- package/dist/home/index.js +3 -0
- package/dist/home/open-folder.d.ts +2 -0
- package/dist/home/open-folder.js +14 -0
- package/dist/host-files/agents.d.ts +20 -0
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +15 -0
- package/dist/host-files/host-files.js +182 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/app/app.js +458 -0
- package/dist/page/app/keys.js +94 -0
- package/dist/page/app/phase.js +30 -0
- package/dist/page/app/served.js +73 -0
- package/dist/page/avatars/avatars.js +19 -0
- package/dist/page/avatars/beanie.webp +0 -0
- package/dist/page/avatars/beard.webp +0 -0
- package/dist/page/avatars/bun.webp +0 -0
- package/dist/page/avatars/cat.webp +0 -0
- package/dist/page/avatars/curls.webp +0 -0
- package/dist/page/avatars/glasses.webp +0 -0
- package/dist/page/avatars/moustache.webp +0 -0
- package/dist/page/avatars/owl.webp +0 -0
- package/dist/page/avatars/short-hair.webp +0 -0
- package/dist/page/avatars/silver-bob.webp +0 -0
- package/dist/page/avatars/thought.js +10 -0
- package/dist/page/avatars/torretta.js +38 -0
- package/dist/page/base.css +160 -0
- package/dist/page/board/board.css +959 -0
- package/dist/page/board/board.js +125 -0
- package/dist/page/board/explain.js +58 -0
- package/dist/page/board/marks.js +160 -0
- package/dist/page/board/motion.js +71 -0
- package/dist/page/board/pointer.js +239 -0
- package/dist/page/board/squares.js +26 -0
- package/dist/page/dialogs/analyses.js +195 -0
- package/dist/page/dialogs/dialog.js +26 -0
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/dialogs/new-game.js +42 -0
- package/dist/page/dialogs/profiles.js +152 -0
- package/dist/page/dialogs/search.js +31 -0
- package/dist/page/dialogs/settings.js +42 -0
- package/dist/page/dialogs/studied.js +39 -0
- package/dist/page/dialogs/study.js +51 -0
- package/dist/page/game/badges.js +50 -0
- package/dist/page/game/browse.js +17 -0
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +51 -0
- package/dist/page/game/ending.js +73 -0
- package/dist/page/game/game.css +577 -0
- package/dist/page/game/move-list.js +72 -0
- package/dist/page/game/opening-book.js +14 -0
- package/dist/page/game/plies.js +82 -0
- package/dist/page/game/result-tile.js +14 -0
- package/dist/page/helps/helps.css +689 -0
- package/dist/page/helps/helps.js +220 -0
- package/dist/page/helps/view.js +127 -0
- package/dist/page/layout/fold-tab.js +11 -0
- package/dist/page/layout/folds.js +43 -0
- package/dist/page/layout/header.js +28 -0
- package/dist/page/layout/layout.css +834 -0
- package/dist/page/lead/lead.js +82 -0
- package/dist/page/lead/view.js +38 -0
- package/dist/page/main.js +8 -0
- package/dist/page/pieces/CREDITS.md +19 -0
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +20 -0
- package/dist/page/pieces/nitida.svg +21 -0
- package/dist/page/pieces/pieces.js +28 -0
- package/dist/page/pieces/torretta-small.svg +20 -0
- package/dist/page/pieces/torretta.svg +21 -0
- package/dist/page/players/card.js +56 -0
- package/dist/page/players/clock-time.js +12 -0
- package/dist/page/players/model-name.js +20 -0
- package/dist/page/profile/profile.js +99 -0
- package/dist/page/profile/settings.js +77 -0
- package/dist/page/sounds/capture.ogg +0 -0
- package/dist/page/sounds/check.ogg +0 -0
- package/dist/page/sounds/draw.ogg +0 -0
- package/dist/page/sounds/flag.ogg +0 -0
- package/dist/page/sounds/mate.ogg +0 -0
- package/dist/page/sounds/move.ogg +0 -0
- package/dist/page/sounds/offer.ogg +0 -0
- package/dist/page/sounds/resign.ogg +0 -0
- package/dist/page/sounds/sounds.js +105 -0
- package/dist/page/style.css +9 -0
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/talk/talk.js +165 -0
- package/dist/page/talk/voice.js +38 -0
- package/dist/page/text.js +711 -0
- package/dist/page/theme.css +292 -0
- package/dist/profiles/avatar.d.ts +23 -0
- package/dist/profiles/avatar.js +84 -0
- package/dist/profiles/front-matter.d.ts +9 -0
- package/dist/profiles/front-matter.js +33 -0
- package/dist/profiles/index.d.ts +3 -0
- package/dist/profiles/index.js +5 -0
- package/dist/profiles/profiles.d.ts +69 -0
- package/dist/profiles/profiles.js +254 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/bell.d.ts +68 -0
- package/dist/server/bell.js +225 -0
- package/dist/server/caller.d.ts +21 -0
- package/dist/server/caller.js +132 -0
- package/dist/server/mcp.d.ts +13 -0
- package/dist/server/mcp.js +207 -0
- package/dist/server/page/analyses.d.ts +23 -0
- package/dist/server/page/analyses.js +65 -0
- package/dist/server/page/files.d.ts +5 -0
- package/dist/server/page/files.js +92 -0
- package/dist/server/page/game.d.ts +6 -0
- package/dist/server/page/game.js +381 -0
- package/dist/server/page/http.d.ts +29 -0
- package/dist/server/page/http.js +68 -0
- package/dist/server/page/page.d.ts +41 -0
- package/dist/server/page/page.js +229 -0
- package/dist/server/page/profiles.d.ts +19 -0
- package/dist/server/page/profiles.js +215 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +4 -0
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The talk on the board page (D-51): the player's words and the AI's answers, kept in game.json beside the PGN, or in talk.json with no game (D-77).
|
|
2
|
+
import { endOf } from "./current-game.js";
|
|
3
|
+
/** The most the player may write in a move, in all their lines, and the most a line of the AI's may hold (D-54); with the helps on, as an analyst's note (D-78). */
|
|
4
|
+
export const talkLimits = { player: 200, ai: 300, aiHelps: 700 };
|
|
5
|
+
/**
|
|
6
|
+
* A new line of the talk, said when the game has `ply` moves: refused when it is empty or too long. The player writes 200 characters a move in all, the
|
|
7
|
+
* words sent with the move included; the AI speaks once a move of theirs, on their time, and its agent's word leaves it an answer to what they write after (D-54, D-72).
|
|
8
|
+
*/
|
|
9
|
+
export function talkLine(current, sayer, text, at, ply = isGame(current) ? current.game.record.moves.length : 0, aiMost = talkLimits.ai) {
|
|
10
|
+
const who = sayer === "agent" ? "ai" : sayer;
|
|
11
|
+
const words = text.trim();
|
|
12
|
+
const most = who === "player" ? wordsLeft(current) : aiMost;
|
|
13
|
+
if (words === "") {
|
|
14
|
+
throw new WordsRefused("There is nothing to say: write the words first.", "empty");
|
|
15
|
+
}
|
|
16
|
+
if (who === "player" && most === 0) {
|
|
17
|
+
throw new WordsRefused(`You have written your ${talkLimits.player} characters until the AI's next move: write again after it.`, "spent");
|
|
18
|
+
}
|
|
19
|
+
if (words.length > most) {
|
|
20
|
+
const more = who === "player" && most < talkLimits.player;
|
|
21
|
+
throw new WordsRefused(`The words are ${words.length} characters long: write at most ${most}${more ? " more until the AI's next move" : ""}.`, more ? "longer-than-left" : "long", most);
|
|
22
|
+
}
|
|
23
|
+
if (who === "ai" && isGame(current)) {
|
|
24
|
+
refuseAiWords(current, sayer === "agent");
|
|
25
|
+
}
|
|
26
|
+
return { who, text: words, ply, at: at.toISOString(), ...(sayer === "agent" ? { agent: true } : {}) };
|
|
27
|
+
}
|
|
28
|
+
/** Words refused, and why, for the page to say it in the player's language: none, the move's characters spent, more than a line or what is left holds. */
|
|
29
|
+
export class WordsRefused extends Error {
|
|
30
|
+
reason;
|
|
31
|
+
most;
|
|
32
|
+
constructor(message, reason, most = 0) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.reason = reason;
|
|
35
|
+
this.most = most;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Whether the player's last line wakes the chat: the first of a move, and between games the first since the AI last spoke (D-54, D-74). */
|
|
39
|
+
export function wakesTheChat(current) {
|
|
40
|
+
return going(current) ? playerLinesThisMove(current).length === 1 : unanswered(current).length === 1;
|
|
41
|
+
}
|
|
42
|
+
/** What the player said that the AI has not answered, but for the words whose keys are in `given`. */
|
|
43
|
+
export function unheardWords(current, given) {
|
|
44
|
+
return unanswered(current).filter(({ key }) => !given.has(key)).map(({ key, line }) => ({ key, text: line.text }));
|
|
45
|
+
}
|
|
46
|
+
/** How far each line of the talk got: the player's by `Heard`, the AI's undefined (D-53). */
|
|
47
|
+
export function heardLines(current, given) {
|
|
48
|
+
const waiting = new Set(unanswered(current).map(({ key }) => key));
|
|
49
|
+
return current.talk.map((line, index) => {
|
|
50
|
+
const key = keyOf(current, index);
|
|
51
|
+
return line.who === "ai" ? undefined : !waiting.has(key) ? "answered" : given.has(key) ? "read" : "sent";
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The lines the player wrote since the AI's last move: those it answers with its next move, and the 200 characters of this move are counted on them. */
|
|
55
|
+
function playerLinesThisMove(current) {
|
|
56
|
+
const since = sinceAiMoved(current);
|
|
57
|
+
return current.talk.filter((line) => line.who === "player" && line.ply >= since);
|
|
58
|
+
}
|
|
59
|
+
/** How many characters the player may still write for this move; between games a whole line each time, with no count (D-74). */
|
|
60
|
+
function wordsLeft(current) {
|
|
61
|
+
if (!going(current)) {
|
|
62
|
+
return talkLimits.player;
|
|
63
|
+
}
|
|
64
|
+
return Math.max(talkLimits.player - playerLinesThisMove(current).reduce((sum, line) => sum + line.text.length, 0), 0);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The AI speaks apart from its move once a move of the player's, on their time, with something to say or to answer (D-72), and never while its move waits:
|
|
68
|
+
* it speaks in the move's comment then. Its agent speaks only first, when the player has written nothing to answer, and its word leaves the chat one answer
|
|
69
|
+
* to what they write after. Between games it talks freely, about the game just over or to start another (D-74).
|
|
70
|
+
*/
|
|
71
|
+
function refuseAiWords(current, byAgent) {
|
|
72
|
+
const { game, ai } = current;
|
|
73
|
+
const since = sinceAiMoved(current);
|
|
74
|
+
if (endOf(game) !== undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (game.position.turn === ai) {
|
|
78
|
+
throw new Error("It is your move: speak in ai_move's comment. Nothing changed.");
|
|
79
|
+
}
|
|
80
|
+
const spoken = current.talk.filter((line) => line.who === "ai" && line.ply >= since);
|
|
81
|
+
if (byAgent) {
|
|
82
|
+
refuseAgentWords(current, spoken);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (spoken.some((line) => line.agent !== true)) {
|
|
86
|
+
throw new Error("You have spoken once for this move: say the rest in ai_move's comment. Nothing changed.");
|
|
87
|
+
}
|
|
88
|
+
if (spoken.length > 0 && unanswered(current).length === 0) {
|
|
89
|
+
throw new Error("You have said a word on the player's time already: speak again only to answer them, else in ai_move's comment. Nothing changed.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** The agent's word on the player's time: one, and none once they have written, since the chat answers them. */
|
|
93
|
+
function refuseAgentWords(current, spoken) {
|
|
94
|
+
if (spoken.length > 0) {
|
|
95
|
+
throw new Error("You have spoken for this move already: say nothing more. Nothing changed.");
|
|
96
|
+
}
|
|
97
|
+
if (unanswered(current).length > 0) {
|
|
98
|
+
throw new Error("The player has written to you, and the AI in the chat answers them: say nothing. Nothing changed.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** How many moves the game had just after the AI's last move, 0 before it: what is said since then waits for its next move. */
|
|
102
|
+
function sinceAiMoved(current) {
|
|
103
|
+
const moves = current.game.record.moves.length;
|
|
104
|
+
// The last move is the AI's when the AI is not to move; else the AI's is the one before it.
|
|
105
|
+
const lastIsAi = current.game.position.turn !== current.ai;
|
|
106
|
+
return lastIsAi ? moves : moves >= 2 ? moves - 1 : 0;
|
|
107
|
+
}
|
|
108
|
+
/** The player's lines after the AI's last words and its last move, each with a key of its own across games. */
|
|
109
|
+
function unanswered(current) {
|
|
110
|
+
const { talk } = current;
|
|
111
|
+
const lastSaid = talk.findLastIndex((line) => line.who === "ai");
|
|
112
|
+
return talk.flatMap((line, index) => index > lastSaid && line.who === "player" && !aiMovedSince(current, line.ply) ? [{ key: keyOf(current, index), line }] : []);
|
|
113
|
+
}
|
|
114
|
+
/** Whether the AI has moved since the game had `ply` moves: the last move is the AI's when the AI is not to move, and two moves on, one of them is. */
|
|
115
|
+
function aiMovedSince(current, ply) {
|
|
116
|
+
if (!isGame(current)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const moves = current.game.record.moves.length;
|
|
120
|
+
return moves > ply + 1 || (moves === ply + 1 && current.game.position.turn !== current.ai);
|
|
121
|
+
}
|
|
122
|
+
function isGame(current) {
|
|
123
|
+
return "game" in current;
|
|
124
|
+
}
|
|
125
|
+
/** Whether a game goes on, with its limits to the talk; none with no game or once it is over. */
|
|
126
|
+
function going(current) {
|
|
127
|
+
return isGame(current) && endOf(current.game) === undefined;
|
|
128
|
+
}
|
|
129
|
+
function keyOf(current, index) {
|
|
130
|
+
return `${current.started}#${index}`;
|
|
131
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CurrentGame } from "./current-game.ts";
|
|
2
|
+
/** The AI's answer to one reply of the player's: the reply and its move in SAN, and why in one sentence. */
|
|
3
|
+
export interface Thought {
|
|
4
|
+
readonly reply: string;
|
|
5
|
+
readonly move: string;
|
|
6
|
+
readonly why: string;
|
|
7
|
+
}
|
|
8
|
+
/** The key of the position the player is to move in: the game, by its start, and how many moves it has. */
|
|
9
|
+
export declare function thoughtKey(current: CurrentGame, ply?: number): string;
|
|
10
|
+
/** Keeps a thought for a position, in place of one for the same reply; thoughts of another position go. The thoughts kept now. */
|
|
11
|
+
export declare function keepThought(folder: string, key: string, thought: Thought): Thought[];
|
|
12
|
+
/** The thought for the player's last move, when the AI is to move and thought of it on their time. */
|
|
13
|
+
export declare function thoughtForLastMove(folder: string, current: CurrentGame): Thought | undefined;
|
|
14
|
+
/** Since when the AI has waited for the player's move in a position: the first time it asked, `now` then, kept with its thoughts. */
|
|
15
|
+
export declare function waitingSince(folder: string, key: string, now: Date): Date;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// What the AI thought on the player's time (D-72): its answers to the player's likely replies, in thoughts.json beside game.json, hidden from the player
|
|
2
|
+
// as its plan is (D-29), and kept only for the position they were thought on: the next position's replace them.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { writeAtomically } from "./files.js";
|
|
7
|
+
/** The key of the position the player is to move in: the game, by its start, and how many moves it has. */
|
|
8
|
+
export function thoughtKey(current, ply = current.game.record.moves.length) {
|
|
9
|
+
return `${current.started}#${ply}`;
|
|
10
|
+
}
|
|
11
|
+
/** Keeps a thought for a position, in place of one for the same reply; thoughts of another position go. The thoughts kept now. */
|
|
12
|
+
export function keepThought(folder, key, thought) {
|
|
13
|
+
const kept = keptAt(folder, key);
|
|
14
|
+
const thoughts = [...kept.thoughts.filter((one) => one.reply !== thought.reply), thought];
|
|
15
|
+
if (thoughts.length > mostThoughts) {
|
|
16
|
+
throw new Error(`You have ${mostThoughts} thoughts for this position already: they are enough. Nothing changed.`);
|
|
17
|
+
}
|
|
18
|
+
write(folder, { ...kept, thoughts });
|
|
19
|
+
return thoughts;
|
|
20
|
+
}
|
|
21
|
+
/** The thought for the player's last move, when the AI is to move and thought of it on their time. */
|
|
22
|
+
export function thoughtForLastMove(folder, current) {
|
|
23
|
+
const { moves } = current.game.record;
|
|
24
|
+
const last = moves.at(-1);
|
|
25
|
+
return last === undefined ? undefined : keptAt(folder, thoughtKey(current, moves.length - 1)).thoughts.find((thought) => thought.reply === last.san);
|
|
26
|
+
}
|
|
27
|
+
/** Since when the AI has waited for the player's move in a position: the first time it asked, `now` then, kept with its thoughts. */
|
|
28
|
+
export function waitingSince(folder, key, now) {
|
|
29
|
+
const kept = keptAt(folder, key);
|
|
30
|
+
if (kept.since !== undefined) {
|
|
31
|
+
return new Date(kept.since);
|
|
32
|
+
}
|
|
33
|
+
write(folder, { ...kept, since: now.toISOString() });
|
|
34
|
+
return now;
|
|
35
|
+
}
|
|
36
|
+
/** The most thoughts kept for a position: a few likely replies, not every legal one. */
|
|
37
|
+
const mostThoughts = 8;
|
|
38
|
+
const fileName = "thoughts.json";
|
|
39
|
+
const thoughtsFile = z.object({
|
|
40
|
+
key: z.string(),
|
|
41
|
+
thoughts: z.array(z.object({ reply: z.string(), move: z.string(), why: z.string() })),
|
|
42
|
+
since: z.iso.datetime().optional(),
|
|
43
|
+
});
|
|
44
|
+
/** What is kept for a position: nothing yet when the file is for another, or missing. */
|
|
45
|
+
function keptAt(folder, key) {
|
|
46
|
+
const kept = read(folder);
|
|
47
|
+
return kept?.key === key ? kept : { key, thoughts: [] };
|
|
48
|
+
}
|
|
49
|
+
function read(folder) {
|
|
50
|
+
try {
|
|
51
|
+
return thoughtsFile.parse(JSON.parse(readFileSync(join(folder, fileName), "utf8")));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// None yet, or one Chesspal cannot read: thoughts are only a help, and the next one written replaces it.
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function write(folder, kept) {
|
|
59
|
+
writeAtomically(join(folder, fileName), `${JSON.stringify(kept, null, 2)}\n`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** What the data folder is found from: the environment, the system, the user's own folder, and where the system says Documents is. */
|
|
2
|
+
export interface System {
|
|
3
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
4
|
+
readonly platform: string;
|
|
5
|
+
readonly home: string;
|
|
6
|
+
/** What `reg query` prints of Documents' place, or undefined when it cannot run. */
|
|
7
|
+
readonly registry: () => string | undefined;
|
|
8
|
+
/** A file's text, or undefined when it cannot be read: the XDG user dirs on Linux. */
|
|
9
|
+
readonly readText: (path: string) => string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** The player's data folder: CHESSPAL_HOME when set, else the system's Documents with Chesspal's folder in it. */
|
|
12
|
+
export declare function dataFolder(system?: System): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The player's data folder (D-71): Documents\My Games\Chesspal on Windows, wherever the system keeps Documents, OneDrive too; Documents/Chesspal
|
|
2
|
+
// elsewhere, where Linux's XDG user dirs put Documents; CHESSPAL_HOME in its place, for the tests and the playtests.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { posix, win32 } from "node:path";
|
|
7
|
+
/** The player's data folder: CHESSPAL_HOME when set, else the system's Documents with Chesspal's folder in it. */
|
|
8
|
+
export function dataFolder(system = thisSystem()) {
|
|
9
|
+
const chosen = system.env.CHESSPAL_HOME?.trim();
|
|
10
|
+
const path = system.platform === "win32" ? win32 : posix;
|
|
11
|
+
if (chosen !== undefined && chosen !== "") {
|
|
12
|
+
return path.resolve(chosen);
|
|
13
|
+
}
|
|
14
|
+
if (system.platform === "win32") {
|
|
15
|
+
return win32.join(windowsDocuments(system), "My Games", "Chesspal");
|
|
16
|
+
}
|
|
17
|
+
return posix.join(system.platform === "darwin" ? posix.join(system.home, "Documents") : xdgDocuments(system), "Chesspal");
|
|
18
|
+
}
|
|
19
|
+
// Where Windows keeps Documents for this user, as Explorer's known folder does: the registry names it, with variables such as %USERPROFILE%.
|
|
20
|
+
const shellFolders = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders";
|
|
21
|
+
function thisSystem() {
|
|
22
|
+
return { env: process.env, platform: process.platform, home: homedir(), registry: readRegistry, readText };
|
|
23
|
+
}
|
|
24
|
+
function readText(path) {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(path, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Documents as the XDG user dirs name it, in the system's language, such as ~/Documenti, as GLib reads them; ~/Documents when they say nothing usable. */
|
|
33
|
+
function xdgDocuments({ env, home, readText }) {
|
|
34
|
+
const config = env.XDG_CONFIG_HOME?.startsWith("/") === true ? env.XDG_CONFIG_HOME : posix.join(home, ".config");
|
|
35
|
+
const named = /^\s*XDG_DOCUMENTS_DIR\s*=\s*"([^"]*)"/m.exec(readText(posix.join(config, "user-dirs.dirs")) ?? "")?.[1];
|
|
36
|
+
const filled = named?.replace(/^\$HOME(?=\/|$)/, home);
|
|
37
|
+
return filled !== undefined && posix.isAbsolute(filled) ? posix.normalize(filled) : posix.join(home, "Documents");
|
|
38
|
+
}
|
|
39
|
+
// About 40 ms, once a process: PowerShell would say the same in 270. reg writes in the console's code page, which breaks a folder with accents: UTF-8 first.
|
|
40
|
+
function readRegistry() {
|
|
41
|
+
const command = `"chcp 65001 >nul & reg query "${shellFolders}" /v Personal"`;
|
|
42
|
+
const answer = spawnSync("cmd.exe", ["/d", "/s", "/c", command], { encoding: "utf8", windowsHide: true, windowsVerbatimArguments: true, timeout: 5_000 });
|
|
43
|
+
return answer.status === 0 ? answer.stdout : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Documents as the registry names it, with its variables filled in; the user's own Documents when the registry says nothing usable. */
|
|
46
|
+
function windowsDocuments({ env, home, registry }) {
|
|
47
|
+
const named = /^\s*Personal\s+REG_(?:EXPAND_)?SZ\s+(.+?)\s*$/m.exec(registry() ?? "")?.[1];
|
|
48
|
+
const filled = named?.replace(/%([^%]+)%/g, (whole, name) => valueOf(env, name) ?? whole);
|
|
49
|
+
return filled !== undefined && win32.isAbsolute(filled) && !/[%�]/.test(filled) ? filled : win32.join(valueOf(env, "USERPROFILE") ?? home, "Documents");
|
|
50
|
+
}
|
|
51
|
+
// Windows's variables, whatever their case.
|
|
52
|
+
function valueOf(env, name) {
|
|
53
|
+
const key = Object.keys(env).find((one) => one.toLowerCase() === name.toLowerCase());
|
|
54
|
+
return key === undefined ? undefined : env[key];
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The data folder shown to the player in the system's own file manager: Explorer, Finder, or the Linux desktop's.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
/** Opens a folder in the system's file manager, without waiting for it to close: whether the manager started, for the page to say so when it did not. */
|
|
4
|
+
export function openFolder(folder, platform) {
|
|
5
|
+
const manager = platform === "win32" ? "explorer.exe" : platform === "darwin" ? "open" : "xdg-open";
|
|
6
|
+
return new Promise((done) => {
|
|
7
|
+
const opened = spawn(manager, [folder], { detached: true, stdio: "ignore" });
|
|
8
|
+
opened.once("spawn", () => {
|
|
9
|
+
opened.unref();
|
|
10
|
+
done(true);
|
|
11
|
+
});
|
|
12
|
+
opened.once("error", () => done(false));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AiLevel } from "../core/index.ts";
|
|
2
|
+
/** The agents that think for the AI, by their names. */
|
|
3
|
+
export declare const ponderAgents: readonly string[];
|
|
4
|
+
/** The coach's agent (D-88). */
|
|
5
|
+
export declare const coachAgent = "chesspal-coach";
|
|
6
|
+
/** The analyst's agent, which reviews a game over on the board page when the player wants it (D-89). */
|
|
7
|
+
export declare const analystAgent = "chesspal-analyst";
|
|
8
|
+
/** The agent that thinks for the AI at a level: on the quick model at the beginner and club levels, on the AI's own above. */
|
|
9
|
+
export declare function ponderAgentFor(level: AiLevel): string;
|
|
10
|
+
/** Whether the AI's thoughts on the player's time come from a quicker model than its own at a level. */
|
|
11
|
+
export declare function onQuickModel(level: AiLevel): boolean;
|
|
12
|
+
/** Whether the player's folder has the agent, as init writes it: a folder set up before it has none, and the AI then plays as before. */
|
|
13
|
+
export declare function hasAgent(folder: string, agent: string): boolean;
|
|
14
|
+
/** The agents' files, as Claude Code reads them from .claude/agents: Chesspal's whole, rewritten by init. */
|
|
15
|
+
export declare function agentFiles(folder: string): {
|
|
16
|
+
path: string;
|
|
17
|
+
text: string;
|
|
18
|
+
}[];
|
|
19
|
+
/** Instructions for the AI, by their file's name in the package's instructions folder. */
|
|
20
|
+
export declare function instructions(name: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The agents that work in the background in Claude Code: those that think for the opponent on the player's time (D-72), one on the host's quick model for the
|
|
2
|
+
// lower levels, one on the AI's own for the higher; the coach, with the helps on (D-88), and the analyst of a game over (D-89), on the AI's own model.
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const quickAgent = "chesspal-ponder-quick";
|
|
6
|
+
const ownAgent = "chesspal-ponder";
|
|
7
|
+
/** The agents that think for the AI, by their names. */
|
|
8
|
+
export const ponderAgents = [quickAgent, ownAgent];
|
|
9
|
+
/** The coach's agent (D-88). */
|
|
10
|
+
export const coachAgent = "chesspal-coach";
|
|
11
|
+
/** The analyst's agent, which reviews a game over on the board page when the player wants it (D-89). */
|
|
12
|
+
export const analystAgent = "chesspal-analyst";
|
|
13
|
+
/** The agent that thinks for the AI at a level: on the quick model at the beginner and club levels, on the AI's own above. */
|
|
14
|
+
export function ponderAgentFor(level) {
|
|
15
|
+
return onQuickModel(level) ? quickAgent : ownAgent;
|
|
16
|
+
}
|
|
17
|
+
/** Whether the AI's thoughts on the player's time come from a quicker model than its own at a level. */
|
|
18
|
+
export function onQuickModel(level) {
|
|
19
|
+
return level === "beginner" || level === "club";
|
|
20
|
+
}
|
|
21
|
+
/** Whether the player's folder has the agent, as init writes it: a folder set up before it has none, and the AI then plays as before. */
|
|
22
|
+
export function hasAgent(folder, agent) {
|
|
23
|
+
return existsSync(agentPath(folder, agent));
|
|
24
|
+
}
|
|
25
|
+
/** The agents' files, as Claude Code reads them from .claude/agents: Chesspal's whole, rewritten by init. */
|
|
26
|
+
export function agentFiles(folder) {
|
|
27
|
+
const ponder = instructions("ponder");
|
|
28
|
+
const thinking = "Chesspal's opponent thinking on the player's time. Started by the AI in the background after its move, as ai_move says.";
|
|
29
|
+
return [
|
|
30
|
+
{ agent: quickAgent, model: "haiku", description: thinking, tools: ["ponder"], text: ponder },
|
|
31
|
+
{ agent: ownAgent, model: "inherit", description: thinking, tools: ["ponder"], text: ponder },
|
|
32
|
+
// Its description tells the chat it starts the agent only as ai_move says, never to ask it about the game (Priya).
|
|
33
|
+
{ agent: coachAgent, model: "inherit", description: "Chesspal's coach. Started only as Chesspal's answers say, in the background, never to answer the player.", tools: ["coach"], text: instructions("coach") },
|
|
34
|
+
{
|
|
35
|
+
agent: analystAgent,
|
|
36
|
+
model: "inherit",
|
|
37
|
+
description: "Chesspal's analyst. Started only as Chesspal's answers say, in the background, to review a game over on the board page; never to answer the player.",
|
|
38
|
+
tools: ["analyse_game", "annotate_game", "look", "position"],
|
|
39
|
+
text: `${instructions("analyst-agent")}\n${instructions("analysis")}`,
|
|
40
|
+
},
|
|
41
|
+
].map(({ agent, model, description, tools, text }) => ({
|
|
42
|
+
path: agentPath(folder, agent),
|
|
43
|
+
// The tools it works with: Claude Code gives an agent its MCP tools loaded, with no search first (Priya).
|
|
44
|
+
text: ["---", `name: ${agent}`, `description: ${description}`, `tools: ${tools.map((tool) => `mcp__chesspal__${tool}`).join(", ")}`, `model: ${model}`, "---", "", text, ""].join("\n"),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
/** Instructions for the AI, by their file's name in the package's instructions folder. */
|
|
48
|
+
export function instructions(name) {
|
|
49
|
+
return readFileSync(new URL(`../../instructions/${name}.md`, import.meta.url), "utf8").trim();
|
|
50
|
+
}
|
|
51
|
+
function agentPath(folder, agent) {
|
|
52
|
+
return join(folder, ".claude", "agents", `${agent}.md`);
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets up the player's folder for Claude Code and Codex (D-31): .mcp.json with the chesspal server, AGENTS.md with the opponent's instructions between
|
|
3
|
+
* Chesspal's marks, CLAUDE.md importing it, the hosts' settings with the server approved, and their hooks. What the player wrote there stays; it returns
|
|
4
|
+
* the files written.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setUpHosts(folder: string, platform: string): string[];
|
|
7
|
+
/**
|
|
8
|
+
* The files an older init wrote in a folder, or left out, that this version would write otherwise, by their paths in it: the AI and the page then ask the
|
|
9
|
+
* player to run init again. None in a folder init never set up.
|
|
10
|
+
*/
|
|
11
|
+
export declare function outdatedHostFiles(folder: string, platform: string): string[];
|
|
12
|
+
/** Where Codex reads a folder's hooks, which it runs only once the player trusts them. */
|
|
13
|
+
export declare function codexHooksPath(folder: string): string;
|
|
14
|
+
/** The instructions of the AI opponent, read from the package wherever it is installed. */
|
|
15
|
+
export declare function opponentInstructions(): string;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// What `chesspal init` writes in a player's folder for each AI host: the opponent's instructions, the MCP server, the hooks and the agents.
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { makeFolder, writeAtomically } from "../games/index.js";
|
|
5
|
+
import { agentFiles, instructions } from "./agents.js";
|
|
6
|
+
/**
|
|
7
|
+
* Sets up the player's folder for Claude Code and Codex (D-31): .mcp.json with the chesspal server, AGENTS.md with the opponent's instructions between
|
|
8
|
+
* Chesspal's marks, CLAUDE.md importing it, the hosts' settings with the server approved, and their hooks. What the player wrote there stays; it returns
|
|
9
|
+
* the files written.
|
|
10
|
+
*/
|
|
11
|
+
export function setUpHosts(folder, platform) {
|
|
12
|
+
makeFolder(folder);
|
|
13
|
+
const written = hostChanges(folder, platform);
|
|
14
|
+
for (const { path, text } of written) {
|
|
15
|
+
makeFolder(join(path, ".."));
|
|
16
|
+
writeAtomically(path, text);
|
|
17
|
+
}
|
|
18
|
+
return written.map(({ path }) => path);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The files an older init wrote in a folder, or left out, that this version would write otherwise, by their paths in it: the AI and the page then ask the
|
|
22
|
+
* player to run init again. None in a folder init never set up.
|
|
23
|
+
*/
|
|
24
|
+
export function outdatedHostFiles(folder, platform) {
|
|
25
|
+
if (!markedBlock.test(textOf(join(folder, "AGENTS.md")) ?? "")) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return hostChanges(folder, platform).map(({ path }) => relative(folder, path));
|
|
29
|
+
}
|
|
30
|
+
/** Where Codex reads a folder's hooks, which it runs only once the player trusts them. */
|
|
31
|
+
export function codexHooksPath(folder) {
|
|
32
|
+
return join(folder, ".codex", "hooks.json");
|
|
33
|
+
}
|
|
34
|
+
/** The instructions of the AI opponent, read from the package wherever it is installed. */
|
|
35
|
+
export function opponentInstructions() {
|
|
36
|
+
return instructions("opponent");
|
|
37
|
+
}
|
|
38
|
+
// The marks around Chesspal's lines in AGENTS.md: init rewrites what lies between them, and the player's own text stays around them.
|
|
39
|
+
const startMark = "<!-- chesspal: these lines up to the end mark are Chesspal's, and chesspal init rewrites them; write your own after it. -->";
|
|
40
|
+
const endMark = "<!-- chesspal: end -->";
|
|
41
|
+
const markedBlock = /<!-- chesspal: (?!end)[^\n]*\n[\s\S]*?<!-- chesspal: end -->/;
|
|
42
|
+
/** What init writes for each host, missing or changed: what the player wrote there stays. */
|
|
43
|
+
function hostChanges(folder, platform) {
|
|
44
|
+
const changes = [mcpFile(folder, platform), agentsFile(folder), claudeFile(folder), claudeSettingsFile(folder), ...ponderAgentChanges(folder), codexConfigFile(folder, platform), codexHooksFile(folder)];
|
|
45
|
+
return changes.filter((change) => change !== undefined);
|
|
46
|
+
}
|
|
47
|
+
/** How a host starts the server: on Windows through cmd, which finds npm's shim. */
|
|
48
|
+
function chesspalServer(platform) {
|
|
49
|
+
return platform === "win32" ? { command: "cmd", args: ["/c", "chesspal", "mcp"] } : { command: "chesspal", args: ["mcp"] };
|
|
50
|
+
}
|
|
51
|
+
/** Claude Code's agents that work on the player's time, thinking for the opponent and coaching, Chesspal's whole: written when they are missing or changed. */
|
|
52
|
+
function ponderAgentChanges(folder) {
|
|
53
|
+
return agentFiles(folder).filter(({ path, text }) => textOf(path) !== text);
|
|
54
|
+
}
|
|
55
|
+
/** .mcp.json with the chesspal server, beside the servers already there, keeping the environment the player gave it, as CHESSPAL_HOME in a playtest. */
|
|
56
|
+
function mcpFile(folder, platform) {
|
|
57
|
+
const path = join(folder, ".mcp.json");
|
|
58
|
+
const config = jsonOf(path);
|
|
59
|
+
const servers = isObject(config.mcpServers) ? config.mcpServers : {};
|
|
60
|
+
const before = isObject(servers.chesspal) ? servers.chesspal : {};
|
|
61
|
+
return changed(path, { ...config, mcpServers: { ...servers, chesspal: { ...chesspalServer(platform), ...(isObject(before.env) ? { env: before.env } : {}) } } });
|
|
62
|
+
}
|
|
63
|
+
/** AGENTS.md with the opponent's instructions between Chesspal's marks: rewritten when they changed, put first in a file that has none. */
|
|
64
|
+
function agentsFile(folder) {
|
|
65
|
+
const path = join(folder, "AGENTS.md");
|
|
66
|
+
const before = textOf(path);
|
|
67
|
+
const block = [startMark, opponentInstructions(), endMark].join("\n");
|
|
68
|
+
if (before === undefined) {
|
|
69
|
+
return { path, text: `${block}\n` };
|
|
70
|
+
}
|
|
71
|
+
const text = markedBlock.test(before) ? before.replace(markedBlock, () => block) : `${block}\n\n${before}`;
|
|
72
|
+
return text === before ? undefined : { path, text };
|
|
73
|
+
}
|
|
74
|
+
/** CLAUDE.md, which Claude Code reads, importing AGENTS.md: the import put first in a file without it. */
|
|
75
|
+
function claudeFile(folder) {
|
|
76
|
+
const path = join(folder, "CLAUDE.md");
|
|
77
|
+
const before = textOf(path);
|
|
78
|
+
if (before === undefined) {
|
|
79
|
+
return { path, text: "@AGENTS.md\n" };
|
|
80
|
+
}
|
|
81
|
+
return before.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md") ? undefined : { path, text: `@AGENTS.md\n${before}` };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Claude Code's settings: the chesspal server of .mcp.json approved and its tools allowed, so that the first chat asks for neither, and the doorbell of the
|
|
85
|
+
* moves on the page (D-47), and the note of how each chat starts (D-57); the player's own stay.
|
|
86
|
+
*/
|
|
87
|
+
function claudeSettingsFile(folder) {
|
|
88
|
+
const path = join(folder, ".claude", "settings.json");
|
|
89
|
+
const settings = jsonOf(path);
|
|
90
|
+
const permissions = isObject(settings.permissions) ? settings.permissions : {};
|
|
91
|
+
const hooks = isObject(settings.hooks) ? settings.hooks : {};
|
|
92
|
+
return changed(path, {
|
|
93
|
+
...settings,
|
|
94
|
+
enabledMcpjsonServers: withItem(settings.enabledMcpjsonServers, "chesspal"),
|
|
95
|
+
permissions: { ...permissions, allow: withItem(permissions.allow, "mcp__chesspal") },
|
|
96
|
+
hooks: withHook(withHook(hooks, "Stop", bellHook), "SessionStart", chatHook),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** The hooks with one of Chesspal's put back beside the player's own whenever it is missing; an event of theirs that is no list stays theirs. */
|
|
100
|
+
function withHook(hooks, event, ours) {
|
|
101
|
+
const entries = hooks[event];
|
|
102
|
+
if (entries !== undefined && !Array.isArray(entries)) {
|
|
103
|
+
return hooks;
|
|
104
|
+
}
|
|
105
|
+
const list = (entries ?? []);
|
|
106
|
+
const there = list.some((entry) => isObject(entry) && Array.isArray(entry.hooks) && entry.hooks.some((hook) => isObject(hook) && hook.command === ours.command));
|
|
107
|
+
return there ? hooks : { ...hooks, [event]: [...list, { hooks: [ours] }] };
|
|
108
|
+
}
|
|
109
|
+
// Claude Code's Stop hook for the doorbell (D-47): it waits in the background from the end of each turn, and wakes the chat only when the player moves on
|
|
110
|
+
// the page; a day at most, until the next turn starts another.
|
|
111
|
+
const bellHook = { type: "command", command: "chesspal listen", asyncRewake: true, timeout: 86_400 };
|
|
112
|
+
// Claude Code's SessionStart hook (D-57): it notes how each chat starts, so that a new game waits for a clean one.
|
|
113
|
+
const chatHook = { type: "command", command: "chesspal chat" };
|
|
114
|
+
/**
|
|
115
|
+
* Codex's settings, read once the player trusts the folder: the chesspal server with its tools approved, a table of Chesspal's rewritten but for the
|
|
116
|
+
* environment the player gave it, as CHESSPAL_HOME in a playtest; the rest stays.
|
|
117
|
+
*/
|
|
118
|
+
function codexConfigFile(folder, platform) {
|
|
119
|
+
const path = join(folder, ".codex", "config.toml");
|
|
120
|
+
const before = textOf(path) ?? "";
|
|
121
|
+
const { command, args } = chesspalServer(platform);
|
|
122
|
+
// The table runs up to the next one, or to the end; a header with a comment after it is the same table.
|
|
123
|
+
const ours = /^\s*\[mcp_servers\.chesspal\]\s*(#.*)?\r?\n[\s\S]*?(?=^\s*\[|(?![\s\S]))/m;
|
|
124
|
+
const environment = (ours.exec(before)?.[0].match(/^\s*env(?:_vars)?\s*=.*$/gm) ?? []).map((line) => `${line.trim()}\n`).join("");
|
|
125
|
+
const table = `[mcp_servers.chesspal]\ncommand = ${JSON.stringify(command)}\nargs = [${args.map((arg) => JSON.stringify(arg)).join(", ")}]\ndefault_tools_approval_mode = "approve"\n${environment}`;
|
|
126
|
+
const rest = before.replace(ours, "").trim();
|
|
127
|
+
const text = rest === "" ? table : `${rest}\n\n${table}`;
|
|
128
|
+
return text === before ? undefined : { path, text };
|
|
129
|
+
}
|
|
130
|
+
/** Codex's hooks, read once the player has trusted them: the doorbell of the moves on the page and how each chat starts, beside the player's own. */
|
|
131
|
+
function codexHooksFile(folder) {
|
|
132
|
+
const path = codexHooksPath(folder);
|
|
133
|
+
const config = jsonOf(path);
|
|
134
|
+
const hooks = isObject(config.hooks) ? config.hooks : {};
|
|
135
|
+
return changed(path, { ...config, hooks: withHook(withHook(hooks, "Stop", codexBellHook), "SessionStart", codexChatHook) });
|
|
136
|
+
}
|
|
137
|
+
// Codex's Stop hook for the doorbell: it holds the turn until the player acts on the page, then Codex goes on; a day at most. On Windows Codex runs
|
|
138
|
+
// it in PowerShell, where `chesspal` would be npm's .ps1 shim, which a strict execution policy stops.
|
|
139
|
+
const codexBellHook = {
|
|
140
|
+
type: "command",
|
|
141
|
+
command: "chesspal listen --codex",
|
|
142
|
+
commandWindows: "chesspal.cmd listen --codex",
|
|
143
|
+
timeout: 86_400,
|
|
144
|
+
statusMessage: "Waiting for your move on the board page",
|
|
145
|
+
};
|
|
146
|
+
// Codex's SessionStart hook: it notes how each chat starts, as Claude Code's does (D-57).
|
|
147
|
+
const codexChatHook = { type: "command", command: "chesspal chat", commandWindows: "chesspal.cmd chat" };
|
|
148
|
+
/** A JSON file's new content, when it differs from what is there. */
|
|
149
|
+
function changed(path, value) {
|
|
150
|
+
const text = `${JSON.stringify(value, null, 2)}\n`;
|
|
151
|
+
return textOf(path) === text ? undefined : { path, text };
|
|
152
|
+
}
|
|
153
|
+
/** A list with an item added when it is missing; a value that is no list becomes one. */
|
|
154
|
+
function withItem(list, item) {
|
|
155
|
+
const items = Array.isArray(list) ? list : [];
|
|
156
|
+
return items.includes(item) ? items : [...items, item];
|
|
157
|
+
}
|
|
158
|
+
/** The object a JSON file holds, empty when there is no file; a file that is not a JSON object stops init with how to fix it. */
|
|
159
|
+
function jsonOf(path) {
|
|
160
|
+
const text = textOf(path);
|
|
161
|
+
if (text === undefined) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
let value;
|
|
165
|
+
try {
|
|
166
|
+
value = JSON.parse(text);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
value = undefined;
|
|
170
|
+
}
|
|
171
|
+
if (!isObject(value)) {
|
|
172
|
+
throw new Error(`${path} is not a JSON object, so Chesspal cannot add itself to it: fix it or remove it, then run chesspal init again.`);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function textOf(path) {
|
|
177
|
+
// Windows PowerShell and Notepad may start a file with a byte order mark, which JSON.parse refuses.
|
|
178
|
+
return existsSync(path) ? readFileSync(path, "utf8").replace(/^/, "") : undefined;
|
|
179
|
+
}
|
|
180
|
+
function isObject(value) {
|
|
181
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
182
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// The files through which an AI host finds Chesspal in the player's folder: the server, the opponent's instructions, the hosts' settings.
|
|
2
|
+
export { analystAgent, coachAgent, hasAgent, instructions, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
|
|
3
|
+
export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.js";
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED