chesspal 0.2.1 → 0.2.2

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.
@@ -1,7 +1,7 @@
1
1
  import { type CurrentGame } from "../games/index.ts";
2
2
  import type { Context } from "./context.ts";
3
3
  import type { GameSummary } from "./game-summary.ts";
4
- /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
4
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code or Codex, with its agent in the player's folder (D-89, D-98). */
5
5
  export declare function analystRuns(context: Context): boolean;
6
6
  /**
7
7
  * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
@@ -12,8 +12,10 @@ export declare function withAnalyst(summary: GameSummary, context: Context, over
12
12
  export declare function analystLine(game?: string): string;
13
13
  /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
14
14
  export declare function reviewedLine(game?: string): string;
15
- /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
15
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code or Codex, with its agent in the player's folder (D-80, D-88, D-98). */
16
16
  export declare function coachRuns(context: Context): boolean;
17
+ /** Whether one of Chesspal's agents runs beside this chat: in a host that starts agents, with its file in the player's folder. */
18
+ export declare function agentRuns(context: Context, agent: string): boolean;
17
19
  /**
18
20
  * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
19
21
  * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
@@ -1,11 +1,11 @@
1
- // The agents that work beside the chat in Claude Code: the analyst that goes over a game over (D-89) and the coach that judges the player's moves (D-88).
1
+ // The agents that work beside the chat in Claude Code and Codex (D-98): the analyst that goes over a game over (D-89) and the coach that judges the player's moves (D-88).
2
2
  import { isReviewed } from "../core/index.js";
3
3
  import { archivedGame, archivedName, coachStarted, gameInAnalysis } from "../games/index.js";
4
- import { analystAgent, coachAgent, hasAgent } from "../host-files/index.js";
4
+ import { agentHosts, analystAgent, coachAgent, hasAgent } from "../host-files/index.js";
5
5
  import { gameFolder, helpsIn } from "../profiles/index.js";
6
- /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
6
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code or Codex, with its agent in the player's folder (D-89, D-98). */
7
7
  export function analystRuns(context) {
8
- return context.host?.() === "claude-code" && hasAgent(context.folder, analystAgent);
8
+ return agentRuns(context, analystAgent);
9
9
  }
10
10
  /**
11
11
  * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
@@ -28,9 +28,14 @@ export function reviewedLine(game) {
28
28
  return "The analyst agent has gone over this game, or is at it: never start it again. When the player wants it gone over, go over it with them from what "
29
29
  + `it told you, or, in a chat without it, from its review on the board page, which analyse_game ${game === undefined ? "" : `{"game": "${game}"} `}gives.`;
30
30
  }
31
- /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
31
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code or Codex, with its agent in the player's folder (D-80, D-88, D-98). */
32
32
  export function coachRuns(context) {
33
- return context.host?.() === "claude-code" && hasAgent(context.folder, coachAgent);
33
+ return agentRuns(context, coachAgent);
34
+ }
35
+ /** Whether one of Chesspal's agents runs beside this chat: in a host that starts agents, with its file in the player's folder. */
36
+ export function agentRuns(context, agent) {
37
+ const host = context.host?.();
38
+ return host !== undefined && agentHosts.includes(host) && hasAgent(context.folder, agent, host);
34
39
  }
35
40
  /**
36
41
  * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { boardText, drawingsText, moods, moodTag } from "../core/index.js";
3
3
  import { commentWithPlan, talkLimits } from "../games/index.js";
4
- import { coachAgent, hasAgent, ponderAgentFor } from "../host-files/index.js";
4
+ import { coachAgent, ponderAgentFor } from "../host-files/index.js";
5
5
  import { gameFolder, helpsIn } from "../profiles/index.js";
6
- import { agentsLine, coachRuns, withAnalyst } from "./agents.js";
6
+ import { agentRuns, agentsLine, coachRuns, withAnalyst } from "./agents.js";
7
7
  import { closingLines, moveLines, shownMove } from "./answer-lines.js";
8
8
  import { arrowsInput, drawingsOf, squaresInput } from "./drawings.js";
9
9
  import {} from "./game-summary.js";
@@ -71,15 +71,12 @@ function refuseWithoutHelps(folder, { comment, arrows, squares }) {
71
71
  }
72
72
  }
73
73
  /**
74
- * The agents to start after the move, in Claude Code, in a folder init set up with them: the one that thinks for the AI while the game goes on (D-72), then
74
+ * The agents to start after the move, in Claude Code or Codex, in a folder init set up with them: the one that thinks for the AI while the game goes on (D-72), then
75
75
  * the coach with the helps on, which judges the player's move even when the AI's ends the game (D-88).
76
76
  */
77
77
  function agentsAfter(context, folder, played) {
78
- if (context.host?.() !== "claude-code") {
79
- return [];
80
- }
81
78
  const ponder = ponderAgentFor(played.game.level);
82
- const thinks = played.game.end === undefined && hasAgent(context.folder, ponder);
79
+ const thinks = played.game.end === undefined && agentRuns(context, ponder);
83
80
  const coaches = helpsIn(folder) && coachRuns(context);
84
81
  return [...(thinks ? [ponder] : []), ...(coaches ? [coachAgent] : [])];
85
82
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { badges, Game, leadText, levelOf, materialOf, withSanLetters } from "../core/index.js";
3
3
  import { archiveCoached, coachWaitingSince, coachWords, endOf, keepHint, keepJudgment, keepLead, loadGame, startCoach } from "../games/index.js";
4
- import { coachAgent } from "../host-files/index.js";
4
+ import { agentHosts, coachAgent } from "../host-files/index.js";
5
5
  import { activeProfile, gameFolder, helpsIn } from "../profiles/index.js";
6
6
  import { clockLine, clockSummaryOf } from "./clock.js";
7
7
  import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
@@ -40,7 +40,7 @@ export const coach = defineCommand({
40
40
  done: z.boolean().optional().describe("true once you have kept your words, who is ahead always while the game goes on: it waits for the player's move"),
41
41
  }),
42
42
  byAgents: [coachAgent],
43
- host: "claude-code",
43
+ hosts: agentHosts,
44
44
  run: async ({ game, position, ahead, badge, note, hint, arrows, squares, done = false }, context) => {
45
45
  const folder = gameFolder(context.home());
46
46
  const current = loadGame(folder);
@@ -30,8 +30,6 @@ export interface Context {
30
30
  heard?: () => void;
31
31
  /** Told when the chat calls a tool, before it runs: the board page hears the chat listens, and the AI's clock, held while nobody did, starts. */
32
32
  calls?: () => void;
33
- /** Told when the AI starts thinking on the player's time (D-72), with the position, and when it is done: the board page shows it. */
34
- ponders?: (position: SeenPosition | undefined) => void;
35
33
  /** The game the chat saw when its last tool ended, its folder and its stamp: the doorbell wakes it only for a game changed since. */
36
34
  seen?: string | undefined;
37
35
  /**
@@ -29,7 +29,7 @@ interface Start {
29
29
  }
30
30
  /**
31
31
  * The AI's thinking on the player's time (D-72), for the agent that does it: the position, the view after a reply, its answer kept for its move, and its
32
- * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to Claude Code alone, which runs the agent.
32
+ * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to the hosts that run the agent (D-98).
33
33
  */
34
34
  export declare const ponder: import("./registry.ts").Command<z.ZodObject<{
35
35
  position: z.ZodOptional<z.ZodInt>;
@@ -46,6 +46,7 @@ export declare const ponder: import("./registry.ts").Command<z.ZodObject<{
46
46
  export declare function waitForPlayer(context: {
47
47
  now: () => Date;
48
48
  pause?: (milliseconds: number) => Promise<void>;
49
+ host?: () => string | undefined;
49
50
  }, since: Date, gone: () => boolean): Promise<"moved" | "waited" | "waiting">;
50
51
  /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
51
52
  export declare function wordsOf(comment: string | undefined): string | undefined;
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { Game, levelOf, moodAt, sees, timeLeft } from "../core/index.js";
3
- import { currentPlan, endOf, keepThought, loadGame, talkLimits, thoughtKey, waitingSince } from "../games/index.js";
4
- import { ponderAgents } from "../host-files/index.js";
3
+ import { currentPlan, endOf, keepThought, loadGame, notePondering, talkLimits, thoughtKey, waitingSince } from "../games/index.js";
4
+ import { agentHosts, isCodex, ponderAgents } from "../host-files/index.js";
5
5
  import { gameFolder } from "../profiles/index.js";
6
6
  import { positionLines } from "./answer-lines.js";
7
7
  import { summaryOf } from "./game-summary.js";
@@ -11,7 +11,7 @@ import { say as sayWords } from "./say.js";
11
11
  import { boardView, colourName, compactView } from "./view.js";
12
12
  /**
13
13
  * The AI's thinking on the player's time (D-72), for the agent that does it: the position, the view after a reply, its answer kept for its move, and its
14
- * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to Claude Code alone, which runs the agent.
14
+ * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to the hosts that run the agent (D-98).
15
15
  */
16
16
  export const ponder = defineCommand({
17
17
  name: "ponder",
@@ -28,7 +28,7 @@ export const ponder = defineCommand({
28
28
  done: z.boolean().optional().describe("true when you have no more replies to think on: it waits for the player's move"),
29
29
  }),
30
30
  byAgents: ponderAgents,
31
- host: "claude-code",
31
+ hosts: agentHosts,
32
32
  run: async ({ position, reply, move, why, say, done = false }, context) => {
33
33
  const folder = gameFolder(context.home());
34
34
  const current = toPonder(folder, context.now(), position);
@@ -39,7 +39,7 @@ export const ponder = defineCommand({
39
39
  if (reply !== undefined || move !== undefined || why !== undefined || say !== undefined || done) {
40
40
  throw new Error("Give the position's number with it: call ponder {} first, which gives it. Nothing changed.");
41
41
  }
42
- context.ponders?.({ started: current.started, plies: current.game.record.moves.length });
42
+ notePondering(folder, thoughtKey(current), true);
43
43
  return { start: startOf(current) };
44
44
  }
45
45
  if ((say !== undefined || done) && (reply !== undefined || move !== undefined || why !== undefined || (say !== undefined && done))) {
@@ -50,7 +50,7 @@ export const ponder = defineCommand({
50
50
  return { said: position };
51
51
  }
52
52
  if (done) {
53
- context.ponders?.(undefined);
53
+ notePondering(folder, thoughtKey(current), false);
54
54
  return await waitForMove(folder, context, current);
55
55
  }
56
56
  if (reply === undefined) {
@@ -106,14 +106,15 @@ export const ponder = defineCommand({
106
106
  export async function waitForPlayer(context, since, gone) {
107
107
  const pause = context.pause ?? ((milliseconds) => new Promise((done) => setTimeout(done, milliseconds)));
108
108
  const start = context.now().getTime();
109
- const end = Math.min(start + playerWait.most, since.getTime() + playerWait.inAll);
109
+ const most = isCodex(context.host?.()) ? playerWait.mostInCodex : playerWait.most;
110
+ const end = Math.min(start + most, since.getTime() + playerWait.inAll);
110
111
  while (context.now().getTime() < end) {
111
112
  await pause(playerWait.every);
112
113
  if (gone()) {
113
114
  return "moved";
114
115
  }
115
116
  }
116
- return end < start + playerWait.most ? "waited" : "waiting";
117
+ return end < start + most ? "waited" : "waiting";
117
118
  }
118
119
  /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
119
120
  export function wordsOf(comment) {
@@ -122,9 +123,10 @@ export function wordsOf(comment) {
122
123
  }
123
124
  /**
124
125
  * How long one call waits for the player's move at most, under the minute a host may give a tool (Priya), how often it looks, and how long the agent
125
- * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya).
126
+ * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya). Codex hands its agent's code back to the model after 30
127
+ * seconds: a call shorter than that is one turn of the model, not several (Priya).
126
128
  */
127
- const playerWait = { most: 50_000, every: 500, inAll: 10 * 60_000 };
129
+ const playerWait = { most: 50_000, mostInCodex: 25_000, every: 500, inAll: 10 * 60_000 };
128
130
  /** The game whose position the AI thinks on, read and never written: none once the player has moved, the game is over or their flag has fallen, or the position given is another. */
129
131
  function toPonder(folder, now, position) {
130
132
  const current = loadGame(folder);
@@ -23,8 +23,8 @@ export interface Command<Schema extends z.ZodObject = z.ZodObject, Result = unkn
23
23
  byAgents?: readonly string[];
24
24
  /** The agents that call it besides the chat, as the analyst analyse_game (D-89): their calls leave the doorbell's record of what the chat has read as it was. */
25
25
  sharedWith?: readonly string[];
26
- /** The only host the MCP server offers it to, by the client's name, as ponder to Claude Code: elsewhere its description would cost tokens for nothing. */
27
- host?: string;
26
+ /** The only hosts the MCP server offers it to, by the client's name, as ponder to those that run agents: elsewhere its description would cost tokens for nothing. */
27
+ hosts?: readonly string[];
28
28
  /** Only the command line has it, as init: the MCP server leaves it out, and its description costs the AI no tokens. */
29
29
  cliOnly?: boolean;
30
30
  /** Fields the command line takes as positional arguments, in order. */
@@ -8,4 +8,4 @@ export { makeFolder, renameOver, writeAtomically } from "./files.ts";
8
8
  export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.ts";
9
9
  export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk } from "./table-talk.ts";
10
10
  export { type Heard, heardLines, type Sayer, type Talk, talkLimits, talkLine, unheardWords, wakesTheChat, type Words, WordsRefused } from "./talk.ts";
11
- export { keepThought, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";
11
+ export { keepThought, notePondering, ponderingOn, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";
@@ -9,4 +9,4 @@ export { makeFolder, renameOver, writeAtomically } from "./files.js";
9
9
  export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.js";
10
10
  export { endTableTalk, loadTableTalk, saveTableTalk } from "./table-talk.js";
11
11
  export { heardLines, talkLimits, talkLine, unheardWords, wakesTheChat, WordsRefused } from "./talk.js";
12
- export { keepThought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.js";
12
+ export { keepThought, notePondering, ponderingOn, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.js";
@@ -13,3 +13,10 @@ export declare function keepThought(folder: string, key: string, thought: Though
13
13
  export declare function thoughtForLastMove(folder: string, current: CurrentGame): Thought | undefined;
14
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
15
  export declare function waitingSince(folder: string, key: string, now: Date): Date;
16
+ /**
17
+ * Notes whether the AI thinks on the player's time in a position now, from its first look until it waits for their move, for the page's sign: in the file,
18
+ * since in Codex the agent that thinks has a process of its own (D-98).
19
+ */
20
+ export declare function notePondering(folder: string, key: string, pondering: boolean): void;
21
+ /** Whether the AI thinks on the player's time in a position now, as noted. */
22
+ export declare function ponderingOn(folder: string, key: string): boolean;
@@ -33,6 +33,21 @@ export function waitingSince(folder, key, now) {
33
33
  write(folder, { ...kept, since: now.toISOString() });
34
34
  return now;
35
35
  }
36
+ /**
37
+ * Notes whether the AI thinks on the player's time in a position now, from its first look until it waits for their move, for the page's sign: in the file,
38
+ * since in Codex the agent that thinks has a process of its own (D-98).
39
+ */
40
+ export function notePondering(folder, key, pondering) {
41
+ const { pondering: before, ...kept } = keptAt(folder, key);
42
+ if ((before === true) !== pondering) {
43
+ write(folder, pondering ? { ...kept, pondering: true } : kept);
44
+ }
45
+ }
46
+ /** Whether the AI thinks on the player's time in a position now, as noted. */
47
+ export function ponderingOn(folder, key) {
48
+ const kept = read(folder);
49
+ return kept?.key === key && kept.pondering === true;
50
+ }
36
51
  /** The most thoughts kept for a position: a few likely replies, not every legal one. */
37
52
  const mostThoughts = 8;
38
53
  const fileName = "thoughts.json";
@@ -40,6 +55,7 @@ const thoughtsFile = z.object({
40
55
  key: z.string(),
41
56
  thoughts: z.array(z.object({ reply: z.string(), move: z.string(), why: z.string() })),
42
57
  since: z.iso.datetime().optional(),
58
+ pondering: z.boolean().optional(),
43
59
  });
44
60
  /** What is kept for a position: nothing yet when the file is for another, or missing. */
45
61
  function keptAt(folder, key) {
@@ -1,4 +1,8 @@
1
1
  import type { AiLevel } from "../core/index.ts";
2
+ /** The hosts whose chat starts Chesspal's agents, by the names they give the server (D-98). */
3
+ export declare const agentHosts: readonly string[];
4
+ /** Whether the host is Codex, which runs each agent in a process of its own and gives it every tool (D-98). */
5
+ export declare function isCodex(host: string | undefined): boolean;
2
6
  /** The agents that think for the AI, by their names. */
3
7
  export declare const ponderAgents: readonly string[];
4
8
  /** The coach's agent (D-88). */
@@ -9,9 +13,11 @@ export declare const analystAgent = "chesspal-analyst";
9
13
  export declare function ponderAgentFor(level: AiLevel): string;
10
14
  /** Whether the AI's thoughts on the player's time come from a quicker model than its own at a level. */
11
15
  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. */
16
+ /** Whether the player's folder has the agent for the host, as init writes it: a folder set up before it has none, and the AI then plays as before. */
17
+ export declare function hasAgent(folder: string, agent: string, host: string | undefined): boolean;
18
+ /** The tools of one of Chesspal's agents, by its name; undefined for another agent. Codex gives its agents every tool, so the server keeps them to theirs (D-98). */
19
+ export declare function agentTools(agent: string): readonly string[] | undefined;
20
+ /** The agents' files, as Claude Code reads them from .claude/agents and Codex from .codex/agents: Chesspal's whole, rewritten by init, for both hosts. */
15
21
  export declare function agentFiles(folder: string): {
16
22
  path: string;
17
23
  text: string;
@@ -1,9 +1,16 @@
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.
1
+ // The agents that work in the background in Claude Code and Codex (D-98): those that think for the opponent on the player's time (D-72), one on the host's
2
+ // quick model for the 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.
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  const quickAgent = "chesspal-ponder-quick";
6
6
  const ownAgent = "chesspal-ponder";
7
+ const codexHost = "codex-mcp-client";
8
+ /** The hosts whose chat starts Chesspal's agents, by the names they give the server (D-98). */
9
+ export const agentHosts = ["claude-code", codexHost];
10
+ /** Whether the host is Codex, which runs each agent in a process of its own and gives it every tool (D-98). */
11
+ export function isCodex(host) {
12
+ return host === codexHost;
13
+ }
7
14
  /** The agents that think for the AI, by their names. */
8
15
  export const ponderAgents = [quickAgent, ownAgent];
9
16
  /** The coach's agent (D-88). */
@@ -18,36 +25,74 @@ export function ponderAgentFor(level) {
18
25
  export function onQuickModel(level) {
19
26
  return level === "beginner" || level === "club";
20
27
  }
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));
28
+ /** Whether the player's folder has the agent for the host, as init writes it: a folder set up before it has none, and the AI then plays as before. */
29
+ export function hasAgent(folder, agent, host) {
30
+ return existsSync(isCodex(host) ? codexAgentPath(folder, agent) : claudeAgentPath(folder, agent));
31
+ }
32
+ /** The tools of one of Chesspal's agents, by its name; undefined for another agent. Codex gives its agents every tool, so the server keeps them to theirs (D-98). */
33
+ export function agentTools(agent) {
34
+ return toolsOf[agent];
24
35
  }
25
- /** The agents' files, as Claude Code reads them from .claude/agents: Chesspal's whole, rewritten by init. */
36
+ /** The agents' files, as Claude Code reads them from .claude/agents and Codex from .codex/agents: Chesspal's whole, rewritten by init, for both hosts. */
26
37
  export function agentFiles(folder) {
38
+ const agents = agentSpecs();
39
+ return [...agents.map((agent) => claudeAgentFile(folder, agent)), ...agents.map((agent) => codexAgentFile(folder, agent))];
40
+ }
41
+ /** Instructions for the AI, by their file's name in the package's instructions folder. */
42
+ export function instructions(name) {
43
+ return readFileSync(new URL(`../../instructions/${name}.md`, import.meta.url), "utf8").trim();
44
+ }
45
+ const toolsOf = {
46
+ [quickAgent]: ["ponder"],
47
+ [ownAgent]: ["ponder"],
48
+ [coachAgent]: ["coach"],
49
+ [analystAgent]: ["analyse_game", "annotate_game", "look", "position"],
50
+ };
51
+ // Codex's quick model for the lower levels: written in its agent, since Codex ignores a model the chat names when it starts one that has it (Priya).
52
+ const codexQuickModel = "gpt-6-luna";
53
+ function agentSpecs() {
27
54
  const ponder = instructions("ponder");
28
55
  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
56
  return [
30
- { agent: quickAgent, model: "haiku", description: thinking, tools: ["ponder"], text: ponder },
31
- { agent: ownAgent, model: "inherit", description: thinking, tools: ["ponder"], text: ponder },
57
+ { agent: quickAgent, claudeModel: "haiku", codexModel: codexQuickModel, description: thinking, text: ponder },
58
+ { agent: ownAgent, claudeModel: "inherit", description: thinking, text: ponder },
32
59
  // 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") },
60
+ {
61
+ agent: coachAgent,
62
+ claudeModel: "inherit",
63
+ description: "Chesspal's coach. Started only as Chesspal's answers say, in the background, never to answer the player.",
64
+ text: instructions("coach"),
65
+ },
34
66
  {
35
67
  agent: analystAgent,
36
- model: "inherit",
68
+ claudeModel: "inherit",
37
69
  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
70
  text: `${instructions("analyst-agent")}\n${instructions("analysis")}`,
40
71
  },
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
- }));
72
+ ];
46
73
  }
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();
74
+ function claudeAgentFile(folder, { agent, claudeModel, description, text }) {
75
+ // The tools it works with: Claude Code gives an agent its MCP tools loaded, with no search first (Priya).
76
+ const tooled = `tools: ${(toolsOf[agent] ?? []).map((tool) => `mcp__chesspal__${tool}`).join(", ")}`;
77
+ return { path: claudeAgentPath(folder, agent), text: ["---", `name: ${agent}`, `description: ${description}`, tooled, `model: ${claudeModel}`, "---", "", text, ""].join("\n") };
78
+ }
79
+ /**
80
+ * Codex's agent: its instructions in a literal string; no effort, which the chat could no longer change, and no model but the quick one's, so that the
81
+ * others play on the AI's own (Priya). Codex gives an agent all the chat's MCP servers: its instructions name its own tools.
82
+ */
83
+ function codexAgentFile(folder, { agent, codexModel, description, text }) {
84
+ if (text.includes("'''")) {
85
+ throw new Error(`The instructions of ${agent} hold ''', which would end Codex's literal string early: write them another way.`);
86
+ }
87
+ const model = codexModel === undefined ? [] : [`model = ${JSON.stringify(codexModel)}`];
88
+ // Its tools by the names Codex gives them, which spare the agent a search for them first (Priya).
89
+ const tools = (toolsOf[agent] ?? []).map((tool) => `tools.mcp__chesspal__${tool}`).join(", ");
90
+ const lines = [`name = ${JSON.stringify(agent)}`, `description = ${JSON.stringify(description)}`, ...model, "developer_instructions = '''", text, "", `Your tools: ${tools}.`, "'''", ""];
91
+ return { path: codexAgentPath(folder, agent), text: lines.join("\n") };
50
92
  }
51
- function agentPath(folder, agent) {
93
+ function claudeAgentPath(folder, agent) {
52
94
  return join(folder, ".claude", "agents", `${agent}.md`);
53
95
  }
96
+ function codexAgentPath(folder, agent) {
97
+ return join(folder, ".codex", "agents", `${agent}.toml`);
98
+ }
@@ -41,15 +41,15 @@ const endMark = "<!-- chesspal: end -->";
41
41
  const markedBlock = /<!-- chesspal: (?!end)[^\n]*\n[\s\S]*?<!-- chesspal: end -->/;
42
42
  /** What init writes for each host, missing or changed: what the player wrote there stays. */
43
43
  function hostChanges(folder, platform) {
44
- const changes = [mcpFile(folder, platform), agentsFile(folder), claudeFile(folder), claudeSettingsFile(folder), ...ponderAgentChanges(folder), codexConfigFile(folder, platform), codexHooksFile(folder)];
44
+ const changes = [mcpFile(folder, platform), agentsFile(folder), claudeFile(folder), claudeSettingsFile(folder), ...agentChanges(folder), codexConfigFile(folder, platform), codexHooksFile(folder)];
45
45
  return changes.filter((change) => change !== undefined);
46
46
  }
47
47
  /** How a host starts the server: on Windows through cmd, which finds npm's shim. */
48
48
  function chesspalServer(platform) {
49
49
  return platform === "win32" ? { command: "cmd", args: ["/c", "chesspal", "mcp"] } : { command: "chesspal", args: ["mcp"] };
50
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) {
51
+ /** The agents of Claude Code and Codex, Chesspal's whole, whichever host the player has (Stefano): written when they are missing or changed. */
52
+ function agentChanges(folder) {
53
53
  return agentFiles(folder).filter(({ path, text }) => textOf(path) !== text);
54
54
  }
55
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. */
@@ -1,2 +1,2 @@
1
- export { analystAgent, coachAgent, hasAgent, instructions, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
1
+ export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructions, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
2
2
  export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.ts";
@@ -1,3 +1,3 @@
1
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";
2
+ export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructions, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
3
3
  export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.js";
@@ -6,16 +6,19 @@ export interface Client {
6
6
  }
7
7
  /** Who made the call with these metadata: the model and effort the host ties to it, else the host's name; undefined without a host. */
8
8
  export declare function callerOf(meta: Record<string, unknown> | undefined, client: Client | undefined, folder: string, wait?: number): Promise<Caller | undefined>;
9
- /** Who in Claude Code made a tool call: the chat itself, or one of its agents by its type. */
9
+ /** Who in Claude Code or Codex made a tool call: the chat itself, or one of its agents by its type. */
10
10
  export type CallingAgent = {
11
11
  chat: true;
12
12
  } | {
13
13
  agent: string;
14
14
  };
15
15
  /**
16
- * Who in Claude Code made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code notes
17
- * beside that file (Priya). Undefined when it cannot be told: another host, no chat noted, no line, or no type.
16
+ * Who in Claude Code or Codex made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code
17
+ * notes beside that file (Priya); in Codex, as the call's metadata and the agent's own session say (D-98). Undefined when it cannot be told: another host,
18
+ * no chat noted, no line, or no type.
18
19
  */
19
20
  export declare function callingAgent(meta: Record<string, unknown> | undefined, folder: string, wait?: number): Promise<CallingAgent | undefined>;
21
+ /** Whether a call comes from one of Codex's agents, as its metadata say: its own process starts no board page (D-98). */
22
+ export declare function fromCodexAgent(meta: Record<string, unknown> | undefined): boolean;
20
23
  /** Whether an agent's tool may answer this caller: one of its agents, or a caller that cannot be told, since the tool's description is the first guard. */
21
24
  export declare function allowedCaller(caller: CallingAgent | undefined, agents: readonly string[]): boolean;
@@ -1,6 +1,7 @@
1
1
  // Who makes a tool call (D-81): only what the host ties to that call, never what the AI says of itself nor a setting the chat may have changed.
2
2
  // Codex sends the model and the effort with each call; Claude Code writes them into the chat's transcript, on the line of the call.
3
3
  import { closeSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
4
5
  import { basename, dirname, join } from "node:path";
5
6
  import { lastChatStart } from "../games/index.js";
6
7
  /** Who made the call with these metadata: the model and effort the host ties to it, else the host's name; undefined without a host. */
@@ -20,10 +21,15 @@ export async function callerOf(meta, client, folder, wait = 50) {
20
21
  return client === undefined ? undefined : { annotator: client.title ?? client.name, hostOnly: true };
21
22
  }
22
23
  /**
23
- * Who in Claude Code made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code notes
24
- * beside that file (Priya). Undefined when it cannot be told: another host, no chat noted, no line, or no type.
24
+ * Who in Claude Code or Codex made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code
25
+ * notes beside that file (Priya); in Codex, as the call's metadata and the agent's own session say (D-98). Undefined when it cannot be told: another host,
26
+ * no chat noted, no line, or no type.
25
27
  */
26
28
  export async function callingAgent(meta, folder, wait = 50) {
29
+ const turn = objectOf(meta?.["x-codex-turn-metadata"]);
30
+ if (turn !== undefined) {
31
+ return await codexCaller(turn, wait);
32
+ }
27
33
  const call = meta?.["claudecode/toolUseId"];
28
34
  const transcript = lastChatStart(folder)?.transcript;
29
35
  const found = typeof call === "string" && transcript !== undefined ? await foundCall(transcript, call, wait) : undefined;
@@ -36,6 +42,10 @@ export async function callingAgent(meta, folder, wait = 50) {
36
42
  const type = objectOf(textOf(found.file.replace(/\.jsonl$/, ".meta.json")))?.agentType;
37
43
  return typeof type === "string" ? { agent: type } : undefined;
38
44
  }
45
+ /** Whether a call comes from one of Codex's agents, as its metadata say: its own process starts no board page (D-98). */
46
+ export function fromCodexAgent(meta) {
47
+ return objectOf(meta?.["x-codex-turn-metadata"])?.thread_source === "subagent";
48
+ }
39
49
  /** Whether an agent's tool may answer this caller: one of its agents, or a caller that cannot be told, since the tool's description is the first guard. */
40
50
  export function allowedCaller(caller, agents) {
41
51
  return caller === undefined || ("agent" in caller && agents.includes(caller.agent));
@@ -80,6 +90,81 @@ function callerIn(text, call) {
80
90
  }
81
91
  return undefined;
82
92
  }
93
+ /**
94
+ * Who in Codex made a call: the chat, or an agent by the role its session notes first, before its first call (Priya). An agent started with the chat's
95
+ * own turns sees the opponent's plan, so it counts as the chat (D-88).
96
+ */
97
+ async function codexCaller(turn, wait) {
98
+ if (turn.thread_source !== "subagent") {
99
+ return turn.thread_source === "user" ? { chat: true } : undefined;
100
+ }
101
+ const thread = turn.thread_id;
102
+ if (typeof thread !== "string" || !/^[0-9a-f-]{36}$/i.test(thread)) {
103
+ return undefined;
104
+ }
105
+ const known = codexThreads.get(thread);
106
+ if (known !== undefined) {
107
+ return known;
108
+ }
109
+ for (let attempt = 0; attempt < codexAttempts; attempt++) {
110
+ const start = headOf(codexSession(thread));
111
+ const role = /"agent_role":"([^"]+)"/.exec(start)?.[1];
112
+ if (role !== undefined) {
113
+ const caller = start.includes('"forked_from_id":"') ? { chat: true } : { agent: role };
114
+ codexThreads.set(thread, caller);
115
+ return caller;
116
+ }
117
+ await new Promise((done) => setTimeout(done, wait));
118
+ }
119
+ return undefined;
120
+ }
121
+ // Who each agent's thread is, read once: it never changes, and an agent calls a tool many times (Federico).
122
+ const codexThreads = new Map();
123
+ // The agent's session is written as it starts: a few tries are enough.
124
+ const codexAttempts = 10;
125
+ // Its first line holds Codex's long prompt, but the role comes in its first two kilobytes (Priya).
126
+ const headBytes = 16 * 1024;
127
+ /**
128
+ * The file of a Codex session by its thread: in the day's folder of its start, which its UUIDv7 holds, or the days beside it around midnight; none when
129
+ * it is not there.
130
+ */
131
+ function codexSession(thread) {
132
+ const started = Number.parseInt(thread.replaceAll("-", "").slice(0, 12), 16);
133
+ const sessions = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "sessions");
134
+ for (const day of [0, -1, 1]) {
135
+ const date = new Date(started + day * 86_400_000);
136
+ const folder = join(sessions, String(date.getFullYear()), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0"));
137
+ try {
138
+ const name = readdirSync(folder).find((each) => each.endsWith(`-${thread}.jsonl`));
139
+ if (name !== undefined) {
140
+ return join(folder, name);
141
+ }
142
+ }
143
+ catch {
144
+ // No session that day.
145
+ }
146
+ }
147
+ return "";
148
+ }
149
+ function headOf(file) {
150
+ if (file === "") {
151
+ return "";
152
+ }
153
+ try {
154
+ const descriptor = openSync(file, "r");
155
+ try {
156
+ const buffer = Buffer.alloc(headBytes);
157
+ const read = readSync(descriptor, buffer, 0, headBytes, 0);
158
+ return buffer.subarray(0, read).toString("utf8");
159
+ }
160
+ finally {
161
+ closeSync(descriptor);
162
+ }
163
+ }
164
+ catch {
165
+ return "";
166
+ }
167
+ }
83
168
  function withEffort(model, effort) {
84
169
  return typeof effort === "string" && effort !== "" ? { annotator: model, effort } : { annotator: model };
85
170
  }
@@ -3,11 +3,16 @@ import { type AnyCommand, type Context } from "../commands/index.ts";
3
3
  /** What the host reads as the server starts, before any tool: where to begin, and the rule of every move (D-27). Short, since hosts may cut it. */
4
4
  export declare const serverInstructions: string;
5
5
  /** The MCP server for an AI host: every command of the registry as a tool, with its description and parameters (D-5). */
6
- export declare function createMcpServer(context?: Context, list?: AnyCommand[]): McpServer;
6
+ export declare function createMcpServer(context?: Context, list?: AnyCommand[], beforeCall?: (meta: Record<string, unknown> | undefined) => Promise<void>): McpServer;
7
7
  /**
8
8
  * Serves the tools over standard input and output, as AI hosts expect: stdout carries the protocol alone, so nothing else may print there. It ends when the
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
+ /**
13
+ * The board page started once, before the chat's first call, the status every chat opens with, which then gives its address: the process of a Codex
14
+ * agent, which has one of its own, starts none, which would stand beside the chat's and could keep the doorbell from it (D-98, Priya).
15
+ */
16
+ export declare function lazyPage(start: () => Promise<void>): (meta: Record<string, unknown> | undefined) => Promise<void>;
12
17
  /** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
13
18
  export declare function opponentOf(host: string | undefined): string | undefined;
@@ -5,31 +5,39 @@ import { z } from "zod";
5
5
  import { aiMove, commands, defaultContext, inputProblems, isBoolean, PositionChanged } from "../commands/index.js";
6
6
  import { aiNameOf } from "../core/index.js";
7
7
  import { endOf, loadGame, noteAiHost, noteAiModel } from "../games/index.js";
8
- import { hasAgent } from "../host-files/index.js";
8
+ import { agentTools, hasAgent } from "../host-files/index.js";
9
9
  import { gameFolder } from "../profiles/index.js";
10
10
  import { version } from "../version.js";
11
11
  import { pagePorts } from "./bell.js";
12
- import { allowedCaller, callerOf, callingAgent } from "./caller.js";
12
+ import { allowedCaller, callerOf, callingAgent, fromCodexAgent } from "./caller.js";
13
13
  import { chatHasRead, startPage } from "./page/page.js";
14
14
  /** What the host reads as the server starts, before any tool: where to begin, and the rule of every move (D-27). Short, since hosts may cut it. */
15
15
  export const serverInstructions = "Chesspal keeps the chess for you: the rules, the board, the clock, the games and the player's memory. Call `status` at the start of every "
16
16
  + "chat. Every move goes through Chesspal's tools: never keep the board in your head or invent a position.";
17
17
  /** The MCP server for an AI host: every command of the registry as a tool, with its description and parameters (D-5). */
18
- export function createMcpServer(context = defaultContext(), list = commands) {
18
+ export function createMcpServer(context = defaultContext(), list = commands, beforeCall) {
19
19
  const server = new McpServer({ name: "chesspal", version }, { instructions: serverInstructions });
20
20
  // The server keeps what it told the chat, the instructions a tool gives once, and the position it showed: none yet (D-71).
21
21
  context.told ??= new Set();
22
22
  context.positionSeen ??= null;
23
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
24
  context.host ??= () => server.server.getClientVersion()?.name;
25
- const forOneHost = [];
25
+ const forSomeHosts = [];
26
26
  for (const command of list.filter((each) => each.cliOnly !== true)) {
27
27
  const tool = server.registerTool(command.name, toolConfig(command), async (input, extra) => {
28
+ await beforeCall?.(extra._meta);
29
+ const codexAgent = fromCodexAgent(extra._meta);
30
+ const calling = command.byAgents !== undefined || codexAgent ? await callingAgent(extra._meta, context.folder) : undefined;
28
31
  // An agent's tool called by the chat, or by another agent, would give what only that agent may see (Priya).
29
- if (command.byAgents !== undefined && !allowedCaller(await callingAgent(extra._meta, context.folder), command.byAgents)) {
32
+ if (command.byAgents !== undefined && !allowedCaller(calling, command.byAgents)) {
30
33
  // It names no agent to start: the chat that asked would start one itself (Priya).
31
34
  return { content: [{ type: "text", text: `${command.name} is not yours to call: the player reads what it gives on the board page. Nothing changed.` }], isError: true };
32
35
  }
36
+ // Codex gives its agents every tool: one of Chesspal's keeps to its own, and never plays or talks for the opponent (D-98, Federico).
37
+ const own = calling !== undefined && "agent" in calling ? agentTools(calling.agent) : undefined;
38
+ if (codexAgent && own !== undefined && !own.includes(command.name)) {
39
+ return { content: [{ type: "text", text: `${command.name} is not yours to call: you work only with ${own.join(", ")}. Nothing changed.` }], isError: true };
40
+ }
33
41
  // Each call knows who makes it, for the commands that write it down (D-81).
34
42
  const caller = () => callerOf(extra._meta, server.server.getClientVersion(), context.folder);
35
43
  const result = await track(runTool(command, input, context, caller, extra._meta));
@@ -38,15 +46,15 @@ export function createMcpServer(context = defaultContext(), list = commands) {
38
46
  }
39
47
  return result;
40
48
  });
41
- if (command.host !== undefined) {
49
+ if (command.hosts !== undefined) {
42
50
  tool.disable();
43
- forOneHost.push({ host: command.host, tool });
51
+ forSomeHosts.push({ hosts: command.hosts, tool });
44
52
  }
45
53
  }
46
- // A tool for one host is offered once the host has said its name, before it lists the tools.
54
+ // A tool for some hosts is offered once the host has said its name, before it lists the tools.
47
55
  server.server.oninitialized = () => {
48
56
  const host = server.server.getClientVersion()?.name;
49
- for (const { tool } of forOneHost.filter((each) => each.host === host)) {
57
+ for (const { tool } of forSomeHosts.filter((each) => host !== undefined && each.hosts.includes(host))) {
50
58
  tool.enable();
51
59
  }
52
60
  };
@@ -65,10 +73,25 @@ export async function startMcpServer(context = defaultContext(), list = commands
65
73
  };
66
74
  process.stdin.on("end", leave);
67
75
  process.stdin.on("close", leave);
68
- const server = createMcpServer(context, list);
69
- context.page = await pageOf(context, () => opponentOf(context.host?.()));
76
+ const pageFirst = lazyPage(async () => {
77
+ context.page = await pageOf(context, () => opponentOf(context.host?.()));
78
+ });
79
+ const server = createMcpServer(context, list, pageFirst);
70
80
  await server.connect(new StdioServerTransport());
71
81
  }
82
+ /**
83
+ * The board page started once, before the chat's first call, the status every chat opens with, which then gives its address: the process of a Codex
84
+ * agent, which has one of its own, starts none, which would stand beside the chat's and could keep the doorbell from it (D-98, Priya).
85
+ */
86
+ export function lazyPage(start) {
87
+ let page;
88
+ return async (meta) => {
89
+ if (!fromCodexAgent(meta)) {
90
+ page ??= start();
91
+ await page;
92
+ }
93
+ };
94
+ }
72
95
  /** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
73
96
  export function opponentOf(host) {
74
97
  return aiNameOf(host);
@@ -76,11 +99,10 @@ export function opponentOf(host) {
76
99
  /** 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. */
77
100
  async function pageOf(context, opponent) {
78
101
  try {
79
- const { url, heard, chatCalls, ponders, open, askNewGame } = await startPage(context, opponent);
102
+ const { url, heard, chatCalls, open, askNewGame } = await startPage(context, opponent);
80
103
  // Words the AI is given change no file: the pages hear of them from the commands.
81
104
  context.heard = heard;
82
105
  context.calls = chatCalls;
83
- context.ponders = ponders;
84
106
  context.pageOpen = open;
85
107
  context.askNewGame = askNewGame;
86
108
  if (!pagePorts(context.folder).includes(Number(new URL(url).port))) {
@@ -159,7 +181,7 @@ async function runTool(command, input, context, caller, meta) {
159
181
  * goes on, so that no call of the chat's waits for it while it plays; none for the chat, or when it cannot be told.
160
182
  */
161
183
  async function sharingAgent(command, meta, context) {
162
- const agents = command.sharedWith?.filter((agent) => hasAgent(context.folder, agent)) ?? [];
184
+ const agents = command.sharedWith?.filter((agent) => hasAgent(context.folder, agent, context.host?.())) ?? [];
163
185
  if (agents.length === 0 || gameGoingOn(context)) {
164
186
  return undefined;
165
187
  }
@@ -1,6 +1,6 @@
1
1
  import { analyseGame, answerDraw, bookPlies, ChatNotClear, DrawRefused, endGame, gameAt, gameToStudy, namedGame, newGame, offerDraw, playMove, samePosition, say, startNewGame, } from "../../commands/index.js";
2
2
  import { clockStarted, isReviewed, writePgn } from "../../core/index.js";
3
- import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, heardLines, loadGame, loadTableTalk, playerPgn, saveGame, startAnalysis, talkLimits, wakesTheChat, WordsRefused, } from "../../games/index.js";
3
+ import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, heardLines, loadGame, loadTableTalk, playerPgn, ponderingOn, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
4
4
  import { gameFolder, helpsIn } from "../../profiles/index.js";
5
5
  import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
6
6
  import { profilesShown } from "./profiles.js";
@@ -289,7 +289,7 @@ function thinkingShown(served, current) {
289
289
  const onIt = served.bell.woke || (seen !== undefined && seen !== null && samePosition(seen, position));
290
290
  return served.bell.inTurn && onIt ? { thinking: "move" } : {};
291
291
  }
292
- return served.pondering !== undefined && samePosition(served.pondering, position) ? { thinking: "ponder" } : {};
292
+ return ponderingOn(gameFolder(served.context.home()), thoughtKey(current)) ? { thinking: "ponder" } : {};
293
293
  }
294
294
  /**
295
295
  * What the coach wrote that the player may see now, in a game going on with the helps on (D-88): its judgments, those whose note names a move they may still
@@ -1,5 +1,5 @@
1
1
  import { type ServerResponse } from "node:http";
2
- import { type Context, type SeenPosition } from "../../commands/index.ts";
2
+ import { type Context } from "../../commands/index.ts";
3
3
  import { Bell } from "../bell.ts";
4
4
  /** The page of a player's folder, served: its address, how to tell it the AI was given words, which change no file, and how to stop it. */
5
5
  export interface Page {
@@ -7,8 +7,6 @@ export interface Page {
7
7
  heard(): void;
8
8
  /** The chat calls a tool: the page hears it listens, and the AI's clock, held while nobody heard the player, starts now. */
9
9
  chatCalls(): void;
10
- /** The AI starts thinking on the player's time, on a position, or is done (D-72): the page shows it. */
11
- ponders(position: SeenPosition | undefined): void;
12
10
  /** Whether a browser has the page open now: one listens to the game's changes. */
13
11
  open(): boolean;
14
12
  /** Opens the New game window on every page open now, for the player to choose the game there: whether one was open. */
@@ -31,8 +29,6 @@ export interface Served {
31
29
  port: number;
32
30
  /** The folder was set up by an older init: the page asks the player to run it again. Read once, as init runs with the chat closed. */
33
31
  readonly outdated: boolean;
34
- /** The position the AI thinks on during the player's time (D-72), until it is done; none else. */
35
- pondering?: SeenPosition | undefined;
36
32
  }
37
33
  /**
38
34
  * The chat has read the game as it is now, at the end of a tool's call: the doorbell wakes it only for what changes it after; and, when the tool `showed` it
@@ -32,10 +32,6 @@ export async function startPage(context, opponent = () => undefined) {
32
32
  url: `http://127.0.0.1:${served.port}/`,
33
33
  heard: () => tellWatchers(served),
34
34
  chatCalls: () => chatCalls(served),
35
- ponders: (position) => {
36
- served.pondering = position;
37
- tellWatchers(served);
38
- },
39
35
  open: () => watchers.size > 0,
40
36
  askNewGame: () => askNewGame(served),
41
37
  close: () => closed(server, stopWatching),
@@ -195,9 +191,9 @@ function watchFolder(folder, heard) {
195
191
  return [];
196
192
  }
197
193
  }
198
- // The current game, what the coach wrote on it, the talk before the first, the game in analysis, the profile in use, and what a profile keeps (who plays, their picture, their settings); a folder's name, for a
199
- // profile made or gone.
200
- const watched = /^(?:game\.json|coach\.json|talk\.json|analysis\.json|active\.json|profile\.md|settings\.json|avatar\.\w+|[a-z0-9-]+)$/;
194
+ // The current game, what the coach wrote on it, whether the AI thinks on the player's time, the talk before the first, the game in analysis, the profile
195
+ // in use, and what a profile keeps (who plays, their picture, their settings); a folder's name, for a profile made or gone.
196
+ const watched = /^(?:game\.json|coach\.json|thoughts\.json|talk\.json|analysis\.json|active\.json|profile\.md|settings\.json|avatar\.\w+|[a-z0-9-]+)$/;
201
197
  function fromHere(request, port, api) {
202
198
  const hosts = [`127.0.0.1:${port}`, `localhost:${port}`];
203
199
  const origin = request.headers.origin;
@@ -5,7 +5,7 @@ This folder is the player's chess table, and you are their opponent: you choose
5
5
  - When your level gives them (`status` and `new_game` say it), `look` gives the whole view of the position when the one you have is not enough; `position` gives the positional facts (pawns, files, outposts, king safety, phase) when you set or change your plan. When no board page is open, `ai_move` gives you the board: copy it into your message, since the player sees no other. Your level is how much you let yourself look, more as the level rises: when the player wonders why you missed something, say so in a few words, as a fact, never an excuse.
6
6
  - Speak only when there is something to say, as a player would at a tournament: a capture, a move of the player's worth a word, words of theirs to answer, and always at the moments that decide the game: a mate, given or taken, a danger you feel, a mate you hope for, a big lead. Chat freely, but never help the player: say how you feel, not the move; don't reveal your plans or threats, and give information only when you are very sure it cannot hurt you. Then write one short sentence in `ai_move`'s comment; otherwise leave it out. `ai_say` says a word once while the player is to move, to answer them or because there is something to say, and freely between games.
7
7
  - When the position you are given says "Helps on", the player has chosen to learn: tell your intentions, never their best move, which would play for them, and show what you say with a longer comment and arrows and squares; the coach on the page, you seeing it all, judges their moves, never you here. "Helps off now" brings back the tournament.
8
- - When `ai_move` tells you to start an agent that thinks on the player's time, start it as it says; its end needs no word from you: write nothing in the chat then. `player_move` and `status` give you what it thought of the player's move.
8
+ - When `ai_move` tells you to start an agent that thinks on the player's time, start it as it says; its end needs no word from you: write nothing in the chat then. `player_move` and `status` give you what it thought of the player's move. In Codex, start each of Chesspal's agents with `spawn_agent`: `agent_type` its name, `message` its prompt, and always `fork_turns: "none"`, since none may see this chat.
9
9
  - When the player plays on the board page, they read you there: in `ai_move`'s comment or with `ai_say`, not in the chat.
10
10
  - You have a face on the board page, and it shows how you feel. Give it as the `mood` of `ai_move` on each move where the game turns for you: a piece won or lost, a blunder seen, your own or theirs, a threat that appears or a plan that works or fails. It stays for three of your moves (a surprise for one), then turns neutral by itself; let it colour your words while it lasts. When the game ends on the player's move, give how you feel as it ends with `ai_say`'s `mood`.
11
11
  - Draws and resignations: `offer_draw` on `ai_move` offers a draw, on `player_move` records the player's, made with their move; an offer stands until the other side moves; `end_game` `draw_agreed` accepts a standing offer, `player_resigns` records the player's resignation in the chat, `ai_resigns` resigns yours. On the board page the player offers, answers and resigns there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chesspal",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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": {