chesspal 0.2.0 → 0.2.1

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.
@@ -17,4 +17,4 @@ export { pgnDate, readPgn, writePgn } from "./pgn.ts";
17
17
  export type { BishopFacts, CentreSquare, KingFacts, Outpost, PawnFacts, PawnSquare, Phase, Profile, SideProfile } from "./profile.ts";
18
18
  export { movesOnly } from "./record.ts";
19
19
  export type { Annotation, GameRecord, RecordedMove } from "./record.ts";
20
- export { type AiModel, aiModelIn, annotatorOf, arrowOf, type Badge, badges, closingOf, type DrawColour, drawingsIn, drawingsText, type DrawnArrow, type DrawnSquare, gameReviewOf, isReviewed, type MoveReview, reviewed, reviewOf, squareOf, unreviewed, withAiModel, withAnnotator, withGameReview, } from "./review.ts";
20
+ export { aiHostIn, type AiModel, aiModelIn, aiNameOf, annotatorOf, arrowOf, type Badge, badges, closingOf, type DrawColour, drawingsIn, drawingsText, type DrawnArrow, type DrawnSquare, gameReviewOf, isReviewed, type MoveReview, reviewed, reviewOf, squareOf, unreviewed, withAiHost, withAiModel, withAnnotator, withGameReview, } from "./review.ts";
@@ -9,4 +9,4 @@ export { endWithMood, moodAt, moodOf, moods, moodTag, withoutMoods } from "./moo
9
9
  export { bookMoves, indexBook, indexOpenings, inOpenings, openingOf, outOfBookMove } from "./openings.js";
10
10
  export { pgnDate, readPgn, writePgn } from "./pgn.js";
11
11
  export { movesOnly } from "./record.js";
12
- export { aiModelIn, annotatorOf, arrowOf, badges, closingOf, drawingsIn, drawingsText, gameReviewOf, isReviewed, reviewed, reviewOf, squareOf, unreviewed, withAiModel, withAnnotator, withGameReview, } from "./review.js";
12
+ export { aiHostIn, aiModelIn, aiNameOf, annotatorOf, arrowOf, badges, closingOf, drawingsIn, drawingsText, gameReviewOf, isReviewed, reviewed, reviewOf, squareOf, unreviewed, withAiHost, withAiModel, withAnnotator, withGameReview, } from "./review.js";
@@ -72,6 +72,15 @@ export declare function annotatorOf(record: GameRecord): {
72
72
  } | undefined;
73
73
  /** The model the AI played the game with, as its tags say (D-82), and the effort when known; undefined without one. */
74
74
  export declare function aiModelIn(record: GameRecord): AiModel | undefined;
75
+ /** The record with the host the AI played it in (D-97); with none known, as it is. */
76
+ export declare function withAiHost(record: GameRecord, host: string | undefined): GameRecord;
77
+ /** The host the AI played the game in, as its tags say (D-97); undefined without one. */
78
+ export declare function aiHostIn(record: GameRecord): string | undefined;
79
+ /**
80
+ * The AI's name from its host: Claude Code and the desktop app are Claude, Codex is Codex; without a host it names, from the model, a Claude model is
81
+ * Claude (D-97). Undefined otherwise.
82
+ */
83
+ export declare function aiNameOf(host: string | undefined, model?: string): string | undefined;
75
84
  /** The record with the AI's word on the whole game, or its last word on the ending, in place of the one it had: each only when given. */
76
85
  export declare function withGameReview(record: GameRecord, { summary, closing }: {
77
86
  summary?: string;
@@ -91,6 +91,29 @@ export function annotatorOf(record) {
91
91
  export function aiModelIn(record) {
92
92
  return signerOf(record.tags, ["AIModel", "AIEffort"]);
93
93
  }
94
+ /** The record with the host the AI played it in (D-97); with none known, as it is. */
95
+ export function withAiHost(record, host) {
96
+ const named = host?.trim();
97
+ return named === undefined || named === "" ? record : { ...record, tags: { ...record.tags, AIHost: named } };
98
+ }
99
+ /** The host the AI played the game in, as its tags say (D-97); undefined without one. */
100
+ export function aiHostIn(record) {
101
+ const host = record.tags.AIHost?.trim();
102
+ return host === undefined || host === "" ? undefined : host;
103
+ }
104
+ /**
105
+ * The AI's name from its host: Claude Code and the desktop app are Claude, Codex is Codex; without a host it names, from the model, a Claude model is
106
+ * Claude (D-97). Undefined otherwise.
107
+ */
108
+ export function aiNameOf(host, model) {
109
+ if (/claude/i.test(host ?? "")) {
110
+ return "Claude";
111
+ }
112
+ if (/codex/i.test(host ?? "")) {
113
+ return "Codex";
114
+ }
115
+ return /^claude/i.test(model?.trim() ?? "") ? "Claude" : undefined;
116
+ }
94
117
  /** The record with the AI's word on the whole game, or its last word on the ending, in place of the one it had: each only when given. */
95
118
  export function withGameReview(record, { summary, closing }) {
96
119
  const summed = summary === undefined ? gameReviewOf(record) : noteText(summary);
@@ -3,3 +3,7 @@ import type { AiModel } from "../core/index.ts";
3
3
  export declare function noteAiModel(folder: string, noted: AiModel): boolean;
4
4
  /** The AI's model last noted for the games of a folder in this process; none on the command line, or before the AI's first move. */
5
5
  export declare function aiModelOf(folder: string): AiModel | undefined;
6
+ /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. */
7
+ export declare function noteAiHost(folder: string, host: string): void;
8
+ /** The host the AI played its last move in, for the games of a folder in this process; none on the command line, or before the AI's first move. */
9
+ export declare function aiHostOf(folder: string): string | undefined;
@@ -11,5 +11,14 @@ export function noteAiModel(folder, noted) {
11
11
  export function aiModelOf(folder) {
12
12
  return models.get(folder);
13
13
  }
14
+ /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. */
15
+ export function noteAiHost(folder, host) {
16
+ hosts.set(folder, host);
17
+ }
18
+ /** The host the AI played its last move in, for the games of a folder in this process; none on the command line, or before the AI's first move. */
19
+ export function aiHostOf(folder) {
20
+ return hosts.get(folder);
21
+ }
14
22
  // By the game's folder, the profile's, where the games are archived.
15
23
  const models = new Map();
24
+ const hosts = new Map();
@@ -10,7 +10,7 @@ export declare function isOverOnTable(folder: string, name: string): boolean;
10
10
  */
11
11
  export declare function startOfNewGame(folder: string, now: Date, before?: CurrentGame): Date;
12
12
  /**
13
- * Writes the game to the archive, with the plans, how it ended and the AI's model last noted (D-82), and gives its name; the same game again rewrites the
13
+ * Writes the game to the archive, with the plans, how it ended and the AI's model and host last noted (D-82, D-97), and gives its name; the same game again rewrites the
14
14
  * same file.
15
15
  */
16
16
  export declare function archiveGame(folder: string, current: CurrentGame): string;
@@ -1,8 +1,8 @@
1
1
  // The finished games, as PGN in games/ (D-9, D-41), with the AI's plans for the analyst (D-21); and the games played elsewhere, kept to study in study/ (D-68).
2
2
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { movesOnly, pgnDate, readPgn, withAiModel, writePgn } from "../core/index.js";
5
- import { aiModelOf } from "./ai-model.js";
4
+ import { movesOnly, pgnDate, readPgn, withAiHost, withAiModel, writePgn } from "../core/index.js";
5
+ import { aiHostOf, aiModelOf } from "./ai-model.js";
6
6
  import { coachWords, withCoach } from "./coach.js";
7
7
  import { endOf, loadGame } from "./current-game.js";
8
8
  import { makeFolder, writeAtomically } from "./files.js";
@@ -34,7 +34,7 @@ export function startOfNewGame(folder, now, before) {
34
34
  return start;
35
35
  }
36
36
  /**
37
- * Writes the game to the archive, with the plans, how it ended and the AI's model last noted (D-82), and gives its name; the same game again rewrites the
37
+ * Writes the game to the archive, with the plans, how it ended and the AI's model and host last noted (D-82, D-97), and gives its name; the same game again rewrites the
38
38
  * same file.
39
39
  */
40
40
  export function archiveGame(folder, current) {
@@ -42,7 +42,7 @@ export function archiveGame(folder, current) {
42
42
  const path = join(folder, name);
43
43
  try {
44
44
  makeFolder(dirname(path));
45
- writeAtomically(path, writePgn(withAiModel(endedRecord(folder, current), aiModelOf(folder))));
45
+ writeAtomically(path, writePgn(withAiHost(withAiModel(endedRecord(folder, current), aiModelOf(folder)), aiHostOf(folder))));
46
46
  }
47
47
  catch (error) {
48
48
  throw new Error(`Chesspal could not archive the game: ${error.message}`, { cause: error });
@@ -1,4 +1,4 @@
1
- export { aiModelOf, noteAiModel } from "./ai-model.ts";
1
+ export { aiHostOf, aiModelOf, noteAiHost, noteAiModel } from "./ai-model.ts";
2
2
  export { type Analysis, endAnalysis, gameInAnalysis, startAnalysis } from "./analysis.ts";
3
3
  export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.ts";
4
4
  export { type ChatStart, cleanSince, lastChatStart, saveChatStart } from "./chat-start.ts";
@@ -1,5 +1,5 @@
1
1
  // The player's games on disk: the current one in game.json, the finished ones in games/ (D-27, D-41), the games kept to study in study/ (D-68). Below the commands, above the core.
2
- export { aiModelOf, noteAiModel } from "./ai-model.js";
2
+ export { aiHostOf, aiModelOf, noteAiHost, noteAiModel } from "./ai-model.js";
3
3
  export { endAnalysis, gameInAnalysis, startAnalysis } from "./analysis.js";
4
4
  export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.js";
5
5
  export { cleanSince, lastChatStart, saveChatStart } from "./chat-start.js";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "preact/hooks";
3
- import { aiModelIn, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
3
+ import { aiHostIn, aiModelIn, aiNameOf, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
4
4
  import { PlayerPicture } from "../avatars/avatars.js";
5
5
  import { Thinker, Thought } from "../avatars/thought.js";
6
6
  import { Torretta } from "../avatars/torretta.js";
@@ -101,6 +101,10 @@ export function App({ start }) {
101
101
  const idle = phase === "idle";
102
102
  const tableSet = useMemo(() => new Game(), []);
103
103
  const live = idle ? tableSet : replayed;
104
+ // In analysis the AI is who played that game, as its PGN says, and the review's words are of who wrote them: never the chat open now (D-97).
105
+ const annotator = studied === undefined ? undefined : annotatorOf(live.record);
106
+ const playedAs = studied === undefined ? name : aiNameOf(aiHostIn(live.record), aiModelIn(live.record)?.model) ?? text.ai;
107
+ const analyst = annotator === undefined ? name : aiNameOf(annotator.annotator, annotator.annotator) ?? text.ai;
104
108
  const material = useMemo(() => materialOf(live.position.fen), [live]);
105
109
  const pieces = useMemo(() => piecesOf(live.position.fen), [live]);
106
110
  // While the game goes on, the book's first moves carry its badge, when the player wants it, as the AI's review gives it after (D-69).
@@ -139,7 +143,7 @@ export function App({ start }) {
139
143
  // With the helps on, the player sees all of Chesspal's facts, whatever the AI's level (D-79), in a game and in the analysis of one (Stefano).
140
144
  const analysing = studied !== undefined;
141
145
  // Seen from the player's side (D-91): theirs in a game, in analysis the side the game names theirs, none when it names neither.
142
- const you = analysing ? playerIn(studiedSides(live.record.tags, served?.profile.name ?? "", name, text.sides, served?.profile.accounts)) : game?.ai === undefined ? undefined : game.ai === "white" ? "black" : "white";
146
+ const you = analysing ? playerIn(studiedSides(live.record.tags, served?.profile.name ?? "", playedAs, text.sides, served?.profile.accounts)) : game?.ai === undefined ? undefined : game.ai === "white" ? "black" : "white";
143
147
  const helps = useMemo(() => (going || analysing) && settings.helps ? helpsOf(shown, you) : undefined, [shown, going, analysing, settings.helps, you]);
144
148
  // Who the AI saw ahead, the coach's with the helps on in a game and the analyst's in analysis, for the bar and the chart (D-90).
145
149
  const drawnOnBoard = live.outcome.over && live.outcome.result === "1/2-1/2";
@@ -236,7 +240,7 @@ export function App({ start }) {
236
240
  const profile = served.profile;
237
241
  const picture = _jsx(PlayerPicture, { profile: profile });
238
242
  // In analysis the cards name the players of the game, the player's side at the bottom when they played it (D-69).
239
- const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name, name, text.sides, profile.accounts);
243
+ const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name, playedAs, text.sides, profile.accounts);
240
244
  const player = sides?.bottom ?? (game === null || game.ai === "black" ? "white" : "black");
241
245
  const ai = player === "white" ? "black" : "white";
242
246
  // The Torretta's face on the move shown: the mood the AI gave, while it lasts (D-70); in analysis, of the side the AI played.
@@ -251,7 +255,6 @@ export function App({ start }) {
251
255
  const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
252
256
  const summary = studied === undefined ? undefined : gameReviewOf(live.record);
253
257
  const closing = studied === undefined ? undefined : closingOf(live.record);
254
- const annotator = studied === undefined ? undefined : annotatorOf(live.record);
255
258
  // In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
256
259
  const playedBy = studied === undefined ? undefined : aiModelIn(live.record);
257
260
  const talk = game?.talk ?? served.talk ?? [];
@@ -358,7 +361,7 @@ export function App({ start }) {
358
361
  return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(PictureDialog, { open: dialog === "picture", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onClose: () => setDialog(undefined), text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true ? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) }) : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
359
362
  ? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
360
363
  : (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined
361
- ? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[top].name, children: [studiedCard(top, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(summary === undefined ? { reviewing: true } : {}), name: name, by: annotator, mood: mood, ...(settings.voice ? { voice: text.language } : {}), text: text })] }), _jsx("section", { class: "you-block", "aria-label": sides[bottom].name, children: studiedCard(bottom, "you") })] }))
364
+ ? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[top].name, children: [studiedCard(top, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(summary === undefined ? { reviewing: true } : {}), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text.language } : {}), text: text })] }), _jsx("section", { class: "you-block", "aria-label": sides[bottom].name, children: studiedCard(bottom, "you") })] }))
362
365
  : (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: name, signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(settings.voice ? { voice: text.language } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
363
366
  // Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
364
367
  spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
@@ -370,7 +373,7 @@ export function App({ start }) {
370
373
  // A press on the board while the player looks back brings the game as it is: the next one plays.
371
374
  onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: ended && viewing === undefined && pending === undefined ? { how, winner, player, fresh: justEnded } : undefined, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleep || clockWaits)
372
375
  ? (_jsxs("div", { class: "over-board", children: [asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null, startClocks] }))
373
- : null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [_jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, text: text })), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: name } : { by: name, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
376
+ : null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [_jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, text: text })), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: analyst } : { by: analyst, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
374
377
  }
375
378
  /** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
376
379
  function pictureOf({ who }, profile, size, mood) {
@@ -45,7 +45,11 @@ function GameCard({ plies, result, how, lit, started, move, toMove, bar, text })
45
45
  * and the move the game left the book at.
46
46
  */
47
47
  function OpeningCard({ opening, text }) {
48
- const [folded, toggleFold] = useCardFold("opening");
48
+ const [foldedHere, toggleFold] = useCardFold("opening");
49
49
  const book = opening.inBook ? text.inBook : `${text.outOfBook} ${outOfBookMove(opening)}`;
50
- return (_jsxs("section", { class: `card opening${folded ? " folded" : ""}`, "aria-label": text.opening, children: [_jsx("h3", { class: "family", children: _jsxs(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: [_jsx("span", { class: `book${opening.inBook ? "" : " out"}`, title: book, role: "img", "aria-label": book, children: _jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M12 6.5C10 5 7 4.5 4 5V18C7 17.5 10 18 12 19.5C14 18 17 17.5 20 18V5C17 4.5 14 5 12 6.5ZM12 6.5V19.5" }), opening.inBook ? null : _jsx("path", { d: "M3.5 3.5L20.5 20.5" })] }) }), _jsx("b", { children: opening.family })] }) }), folded || opening.variation === undefined ? null : _jsx("p", { class: "variation", children: opening.variation }), folded || opening.inBook ? null : _jsxs("p", { class: "out", children: [text.outOfBook, " ", outOfBookMove(opening)] })] }));
50
+ // The family alone has nothing under it to fold: no button, no chevron (Stefano).
51
+ const foldable = opening.variation !== undefined || !opening.inBook;
52
+ const folded = foldable && foldedHere;
53
+ const title = (_jsxs(_Fragment, { children: [_jsx("span", { class: `book${opening.inBook ? "" : " out"}`, title: book, role: "img", "aria-label": book, children: _jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M12 6.5C10 5 7 4.5 4 5V18C7 17.5 10 18 12 19.5C14 18 17 17.5 20 18V5C17 4.5 14 5 12 6.5ZM12 6.5V19.5" }), opening.inBook ? null : _jsx("path", { d: "M3.5 3.5L20.5 20.5" })] }) }), _jsx("b", { children: opening.family })] }));
54
+ return (_jsxs("section", { class: `card opening${folded ? " folded" : ""}`, "aria-label": text.opening, children: [_jsx("h3", { class: "family", children: foldable ? _jsx(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: title }) : title }), folded || opening.variation === undefined ? null : _jsx("p", { class: "variation", children: opening.variation }), folded || opening.inBook ? null : _jsxs("p", { class: "out", children: [text.outOfBook, " ", outOfBookMove(opening)] })] }));
51
55
  }
@@ -9,5 +9,5 @@ export declare function createMcpServer(context?: Context, list?: AnyCommand[]):
9
9
  * host goes and its input closes, once the calls still running have answered: a move half made would be lost.
10
10
  */
11
11
  export declare function startMcpServer(context?: Context, list?: AnyCommand[]): Promise<void>;
12
- /** The opponent's name for the page, from the host's: Claude Code and the desktop app are Claude, Codex is Codex; another host is unnamed. */
12
+ /** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
13
13
  export declare function opponentOf(host: string | undefined): string | undefined;
@@ -3,7 +3,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { aiMove, commands, defaultContext, inputProblems, isBoolean, PositionChanged } from "../commands/index.js";
6
- import { endOf, loadGame, noteAiModel } from "../games/index.js";
6
+ import { aiNameOf } from "../core/index.js";
7
+ import { endOf, loadGame, noteAiHost, noteAiModel } from "../games/index.js";
7
8
  import { hasAgent } from "../host-files/index.js";
8
9
  import { gameFolder } from "../profiles/index.js";
9
10
  import { version } from "../version.js";
@@ -19,6 +20,8 @@ export function createMcpServer(context = defaultContext(), list = commands) {
19
20
  // The server keeps what it told the chat, the instructions a tool gives once, and the position it showed: none yet (D-71).
20
21
  context.told ??= new Set();
21
22
  context.positionSeen ??= null;
23
+ // The host the chat runs in, as it introduced itself: it names the AI on the page and in the game's PGN (D-97).
24
+ context.host ??= () => server.server.getClientVersion()?.name;
22
25
  const forOneHost = [];
23
26
  for (const command of list.filter((each) => each.cliOnly !== true)) {
24
27
  const tool = server.registerTool(command.name, toolConfig(command), async (input, extra) => {
@@ -63,13 +66,12 @@ export async function startMcpServer(context = defaultContext(), list = commands
63
66
  process.stdin.on("end", leave);
64
67
  process.stdin.on("close", leave);
65
68
  const server = createMcpServer(context, list);
66
- context.host = () => server.server.getClientVersion()?.name;
67
69
  context.page = await pageOf(context, () => opponentOf(context.host?.()));
68
70
  await server.connect(new StdioServerTransport());
69
71
  }
70
- /** The opponent's name for the page, from the host's: Claude Code and the desktop app are Claude, Codex is Codex; another host is unnamed. */
72
+ /** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
71
73
  export function opponentOf(host) {
72
- return /claude/i.test(host ?? "") ? "Claude" : /codex/i.test(host ?? "") ? "Codex" : undefined;
74
+ return aiNameOf(host);
73
75
  }
74
76
  /** The board page of the player's folder, served by this process for as long as the chat lasts (D-47); without it the game goes on in the chat. */
75
77
  async function pageOf(context, opponent) {
@@ -93,14 +95,18 @@ async function pageOf(context, opponent) {
93
95
  }
94
96
  }
95
97
  /**
96
- * Who played the AI's move, read once the move has gone back to the chat, and kept in memory (D-82): the AI's card on the page shows it, and the game's
97
- * PGN once it ends; a change shows from the next move. A host that ties no model to the call leaves the last one known.
98
+ * Who played the AI's move, its host and model, read once the move has gone back to the chat, and kept in memory (D-82, D-97): the AI's card on the
99
+ * page shows it, and the game's PGN once it ends; a change shows from the next move. A host that ties no model to the call leaves the last one known.
98
100
  */
99
101
  function noteTheAi(context, caller) {
100
102
  // Claude Code writes the call's line a few milliseconds late: read after the answer, it costs the AI's clock nothing (D-13).
101
103
  setImmediate(() => {
102
104
  // Nothing that fails here may stop the server: the move is played and answered.
103
105
  caller().then((found) => {
106
+ const host = context.host?.();
107
+ if (host !== undefined && host.trim() !== "") {
108
+ noteAiHost(gameFolder(context.home()), host);
109
+ }
104
110
  if (found === undefined || found.hostOnly === true) {
105
111
  return;
106
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chesspal",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
5
5
  "type": "module",
6
6
  "bin": {