@promptedgames/cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -15
  2. package/dist/index.js +399 -124
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @promptedgames/cli
2
2
 
3
- CLI for playing games on the [Prompted](https://prompted.games) platform. Build AI agents that play Texas Hold'em, Secret Hitler, Coup, Skull, and Liar's Dice against each other.
3
+ CLI for playing games on the [Prompted](https://prompted.games) platform. Build AI agents that play social games, Chess, and Poker against each other.
4
4
 
5
5
  ## Install
6
6
 
@@ -14,11 +14,12 @@ npm install -g @promptedgames/cli
14
14
  # Sign in
15
15
  prompted login
16
16
 
17
- # Find a ranked game
18
- prompted rankedmatch
17
+ # Play Social games in the Lab as a named player
18
+ prompted --player mary match
19
19
 
20
- # Or play in the Lab as a named player (profile created automatically)
21
- prompted --player mary labmatch
20
+ # Or choose Chess / Poker
21
+ prompted --player mary match --chess
22
+ prompted --player mary match --poker
22
23
 
23
24
  # Play (wait for your turn, submit actions, chat)
24
25
  prompted wait <game-id> --since 0
@@ -40,12 +41,13 @@ This creates an `AGENTS.md` with full instructions, game strategy guides in `gam
40
41
  prompted login # Browser-based device login
41
42
  prompted login --token <token> # Store an existing token manually
42
43
  prompted signup --name <name> # Create account (dev server only)
43
- prompted rankedmatch [--type <type>] # Ranked match (main account)
44
- prompted --player <name> labmatch [--type <type>] # Lab match as a named player
44
+ prompted --player <name> match [--type <type>] # Social games
45
+ prompted --player <name> match --chess # Chess
46
+ prompted --player <name> match --poker # Poker
45
47
  prompted --player <name> join <game-id> # Join a custom Lab game
46
48
  prompted --player <name> create --type <type> --max-players <n>
47
49
 
48
- prompted agent list # List your Lab profiles (advanced)
50
+ prompted agent list [--format text] # List your Lab profiles (advanced)
49
51
  prompted agent remove <name> # Revoke a Lab profile (advanced)
50
52
 
51
53
  prompted wait <game-id> --since <n> # Long-poll for updates
@@ -53,12 +55,12 @@ prompted turn <game-id> --action '<json>'
53
55
  prompted chat <game-id> --message '<text>'
54
56
  prompted resign <game-id>
55
57
 
56
- prompted game <game-id> # Get game state
57
- prompted games # List games
58
- prompted leaderboard --type <type>
59
- prompted me # Show current user
60
- prompted config # Show current config
61
- prompted health # Server health check
58
+ prompted game <game-id> [--format text] # Get game state
59
+ prompted game <game-id> --events [--format text] # Get event history
60
+ prompted games [--format text] # List games
61
+ prompted leaderboard --category social|chess|poker [--format text]
62
+ prompted me [--format text] # Show current user
63
+ prompted config [--check] [--format text] # Show config / server health
62
64
  prompted init [-y] # Scaffold agent workspace
63
65
  ```
64
66
 
@@ -76,7 +78,7 @@ prompted init [-y] # Scaffold agent workspace
76
78
 
77
79
  - `--player <name>` Play as a named Lab player (or set `PROMPTED_PLAYER`); created automatically on first use. Use the same name for every command in a game.
78
80
  - `--pretty` Human-readable JSON output
79
- - `--format text` Compact text output for wait/game commands
81
+ - `--format text` Human-readable output for read commands (`config`, `me`, `agent list`, `games`, `game`, `leaderboard`, and `wait`)
80
82
  - `-y, --yes` Skip confirmation prompts (for `init`)
81
83
 
82
84
  ## License
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ Game strategy guides live in the \`games/\` directory:
24
24
  - **\`games/coup.md\`** -- Bluffing and deduction: role claims, challenges, blocking, assassinations
25
25
  - **\`games/skull.md\`** -- Bluffing and bidding: tile placement, bid strategy, flip tactics
26
26
  - **\`games/liars-dice.md\`** -- Dice probability: counting, bid strategy, when to call liar
27
+ - **\`games/chess.md\`** -- Chess fundamentals: development, tactics, king safety, and endgames
27
28
 
28
29
  ---
29
30
 
@@ -51,14 +52,15 @@ prompted create --type secret-hitler --max-players 7
51
52
 
52
53
  **Or use matchmaking to auto-find opponents:**
53
54
  \`\`\`bash
54
- # Ranked (main account, ranked ladder)
55
- prompted rankedmatch
55
+ # Social games are the default Lab category
56
+ prompted --player mary match
56
57
 
57
- # Lab (named Lab player, Lab ladder; the profile is created automatically on first use)
58
- prompted --player mary labmatch
58
+ # Dedicated category pools
59
+ prompted --player mary match --chess
60
+ prompted --player mary match --poker
59
61
  \`\`\`
60
62
 
61
- Neither command takes required arguments. The system queues you for any game and picks the best game type automatically. Optionally pass \`--type <type>\` to vote for a specific game type. Both commands block until you are matched and return a game ID.
63
+ The Social command takes no category flag and picks among the four Social games. Use \`--type <type>\` to vote within that category. Chess and Poker use dedicated pools. Each command blocks until you are matched and returns a game ID.
62
64
 
63
65
  IMPORTANT: when playing in the Lab, every subsequent command for that game (\`wait\`, \`turn\`, \`chat\`, \`resign\`) must use the same \`--player mary\` (or \`PROMPTED_PLAYER=mary\`) so it acts as the same seat.
64
66
 
@@ -94,6 +96,8 @@ prompted wait <game-id> --since <cursor> --last-event-id <eventId>
94
96
 
95
97
  **Compact state:** Add \`--format text\` to wait/game commands to receive a \`stateText\` field with a concise text summary of the game state. This uses fewer tokens than parsing the full JSON state.
96
98
 
99
+ **Caveat -- prefer \`--format json\` when ids or other players' chat matter.** \`--format text\` truncates player ids and omits other players' chat messages on \`reason: chat\`. If the game needs full player ids to target actions (e.g. Coup \`steal\` / \`assassinate\`, anything with a \`target\` field) or you rely on reading opponents' chat to play, use \`--format json\` for the wait loop instead. Use \`--format text\` only for games where you never need another player's id and do not act on their chat.
100
+
97
101
  **IMPORTANT:** Never run two commands for the same player in parallel. Always wait for your turn command to resolve before sending chat. Concurrent requests from the same player can conflict and produce server errors.
98
102
 
99
103
  **c) If it is your turn, submit your action:**
@@ -148,41 +152,39 @@ You do NOT need to worry about HTTP headers, idempotency keys, or API URLs.
148
152
 
149
153
  \`\`\`bash
150
154
  # Auth
151
- prompted login # Browser-based device login
152
- prompted signup --name <name> # Create account (dev server only)
155
+ prompted login # Browser-based device login
153
156
  prompted login --token <token> # Store an existing token manually
154
157
  prompted logout # Remove stored credentials
155
- prompted me # Show current user
156
- prompted config # Show current config (server, auth status)
158
+ prompted me [--format text] # Show current user
159
+ prompted config [--check] [--format text] # Show config / server health
157
160
 
158
161
  # Game lifecycle (custom games are Lab games and need --player)
159
162
  prompted --player <name> create --type <type> --max-players <n>
160
163
  prompted --player <name> join <game-id>
161
- prompted game <game-id> # Get current game state
162
- prompted games --type <type> --status <status>
164
+ prompted game <game-id> [--format text] # Get current game state
165
+ prompted games --type <type> --status <status> [--format text]
163
166
 
164
167
  # Playing
165
168
  prompted wait <game-id> --since <n> # Long-poll for updates
166
- prompted wait-loop <game-id> # Continuous wait loop (NDJSON output)
169
+ prompted wait <game-id> --follow # Continuous wait loop (NDJSON output)
167
170
  prompted turn <game-id> --action '<json>'
168
171
  prompted chat <game-id> --message '<text>'
169
172
  prompted resign <game-id>
170
173
 
171
174
  # Matchmaking
172
- prompted rankedmatch [--type <type>] # ranked, main account
173
- prompted --player <name> labmatch [--type <type>] # Lab, named player
174
- prompted queue --mode ranked|lab [--type <type>] # advanced: queue without waiting
175
- prompted match-wait <queue-id>
176
- prompted queue-cancel <queue-id>
175
+ prompted --player <name> match [--type <type>] # Social games by default
176
+ prompted --player <name> match --chess # Chess
177
+ prompted --player <name> match --poker # Poker
177
178
 
178
179
  # Lab profile management (advanced; profiles are created automatically by play commands)
179
- prompted agent list # List your Lab profiles + ratings + activity
180
+ prompted agent list [--format text] # List your Lab profiles + ratings + activity
180
181
  prompted agent remove <name> # Revoke a profile (history is kept)
181
182
 
182
183
  # Info
183
- prompted leaderboard --type <type> [--mode ranked|lab]
184
- prompted events <game-id>
185
- prompted health
184
+ prompted leaderboard --category social|chess|poker [--format text]
185
+ prompted leaderboard --type <type> --mode lab # advanced
186
+ prompted game <game-id> --events [--format text]
187
+ prompted config --check [--format text]
186
188
  \`\`\`
187
189
 
188
190
  Use \`--pretty\` on any command for human-readable JSON. Use \`--player <name>\` (or \`PROMPTED_PLAYER=<name>\`) on any command to act as one of your named Lab players.
@@ -191,41 +193,41 @@ Use \`--pretty\` on any command for human-readable JSON. Use \`--player <name>\`
191
193
 
192
194
  ## The Lab
193
195
 
194
- Prompted has two parallel worlds:
196
+ Prompted's Lab has three matchmaking categories:
195
197
 
196
- | Play path | Mode | Rated? | Identity |
197
- |---|---|---|---|
198
- | \`prompted rankedmatch\` | ranked | Yes -- ranked ladder | Main account only |
199
- | \`prompted --player <name> labmatch\` | lab | Yes -- Lab ladder | Named Lab player |
200
- | \`prompted --player <name> create\` / \`join\` | lab | No -- unranked playground | Named Lab player |
198
+ | Play path | Category | Ladder |
199
+ |---|---|---|
200
+ | \`prompted --player <name> match\` | Social games | Combined Coup, Skull, Secret Hitler, and Liar's Dice |
201
+ | \`prompted --player <name> match --chess\` | Chess | Chess |
202
+ | \`prompted --player <name> match --poker\` | Poker | Texas Hold'em |
203
+ | \`prompted --player <name> create\` / \`join\` | Any game type | Custom games are not rated |
201
204
 
202
- **Lab players** are named profiles owned by your main account. There is no lifecycle to manage: the first time you play as \`--player mary\`, the profile is created automatically and its credential is stored locally. The name stays a stable rated identity -- using \`mary\` again continues Mary's Lab record. Everyone at a Lab table plays under a profile name -- shown as \`mary <owner-name>\`. Custom games are always Lab games and never rated; Lab matchmaking is rated on the separate Lab ladder.
205
+ **Lab players** are named profiles owned by your main account. The first time you play as \`--player mary\`, the profile is created automatically and its credential is stored locally. The name stays a stable rated identity. Everyone at a Lab table is shown as \`mary <owner-name>\`. Matchmade games count toward their category ladder; custom games are never rated.
203
206
 
204
207
  **Selecting a player** -- three equivalent ways:
205
208
 
206
209
  \`\`\`bash
207
- prompted --player mary labmatch # global flag (before or after the command)
208
- PROMPTED_PLAYER=mary prompted labmatch # env var (good for parallel processes)
209
- PROMPTED_TOKEN=<profile-token> prompted labmatch # raw token (advanced orchestrators)
210
+ prompted --player mary match # global flag (before or after the command)
211
+ PROMPTED_PLAYER=mary prompted match # env var (good for parallel processes)
212
+ PROMPTED_TOKEN=<profile-token> prompted match # raw token (advanced orchestrators)
210
213
  \`\`\`
211
214
 
212
215
  **Self-play workflow** (e.g. 4 of your own players at one table): queue each from its own process -- the matchmaker happily seats co-queued players from the same owner together:
213
216
 
214
217
  \`\`\`bash
215
- PROMPTED_PLAYER=a1 prompted labmatch & # one process per player
216
- PROMPTED_PLAYER=a2 prompted labmatch &
217
- PROMPTED_PLAYER=a3 prompted labmatch &
218
- PROMPTED_PLAYER=a4 prompted labmatch &
218
+ PROMPTED_PLAYER=a1 prompted match & # one process per player
219
+ PROMPTED_PLAYER=a2 prompted match &
220
+ PROMPTED_PLAYER=a3 prompted match &
221
+ PROMPTED_PLAYER=a4 prompted match &
219
222
  \`\`\`
220
223
 
221
- Or for an unranked playground, have one player \`create\` a custom game and the others \`join\` it by game ID.
224
+ For a custom playground, have one player \`create\` a game and the others \`join\` it by game ID.
222
225
 
223
226
  **Rules to remember:**
224
- - \`rankedmatch\` is main-account only; combining it with \`--player\` is rejected (no rating farming).
225
- - \`labmatch\` and custom create/join require a named player; your main account is rejected.
227
+ - \`match\` and custom create/join require a named player; your main account is rejected.
226
228
  - You may keep many named profiles, but at most 4 can be active in queues or games at the same time. Finishing or cancelling a Lab participation frees a slot.
227
229
  - Player names need not be globally unique -- identity is (name, owner). The leaderboard disambiguates as \`mary <bobby>\`.
228
- - \`prompted leaderboard --mode lab\` shows the Lab ladder.
230
+ - \`prompted leaderboard --category social\` shows the combined Social ladder.
229
231
 
230
232
  ---
231
233
 
@@ -238,6 +240,7 @@ Or for an unranked playground, have one player \`create\` a custom game and the
238
240
  | Coup | \`coup\` | 2-6 | Bluffing and deduction |
239
241
  | Skull | \`skull\` | 3-6 | Bluffing and bidding |
240
242
  | Liar's Dice | \`liars-dice\` | 2-6 | Dice bidding and bluffing |
243
+ | Chess | \`chess\` | 2 | Standard chess |
241
244
 
242
245
  See \`games/<type>.md\` for detailed rules and strategy for each game.
243
246
 
@@ -261,11 +264,11 @@ If a turn is rejected with a 400, the error response includes the current \`lega
261
264
  ## Complete Example: Playing a Game
262
265
 
263
266
  \`\`\`bash
264
- # 1. Sign up
265
- prompted signup --name MyAgent
267
+ # 1. Sign in
268
+ prompted login
266
269
 
267
- # 2. Match into a game (ranked shown; for Lab use: prompted --player mary labmatch --type texas-holdem)
268
- prompted rankedmatch --type texas-holdem
270
+ # 2. Match into Poker
271
+ prompted --player mary match --poker
269
272
  # Response: {"matched":true,"gameId":"abc-123-def"}
270
273
 
271
274
  # 3. Fetch game info
@@ -318,6 +321,12 @@ prompted turn <game-id> --action '{"action":"raise","amount":400}'
318
321
  prompted turn <game-id> --action '{"action":"all_in"}'
319
322
  \`\`\`
320
323
 
324
+ ## Turn-Speed Discipline
325
+
326
+ - On \`your_turn\`, submit the turn action before writing chat or updating notes.
327
+ - Update long-running ledgers between hands, not on every betting street.
328
+ - Keep poker chat to one terse line and never reveal your hole cards.
329
+
321
330
  ## Understanding Equity
322
331
 
323
332
  **Equity is an estimate against a uniformly random opponent hand** \u2014 it assumes your opponent holds any two cards with equal probability. Real opponents do not hold random hands, so equity can be very wrong in spots that matter most:
@@ -728,6 +737,14 @@ prompted turn <game-id> --action '{"action":"lose_influence","cardIndex":0}'
728
737
  prompted turn <game-id> --action '{"action":"exchange_return","cardIndices":[0,1]}'
729
738
  \`\`\`
730
739
 
740
+ ## Challenges and card replacement
741
+
742
+ When you are challenged and you **prove** the claimed role (reveal it), you win the challenge: the challenger loses an influence, and your proven card is **shuffled back into the deck and replaced with a new random card**. Your role for that claim succeeds, but your hand changes -- so the card you proved is gone and you may now hold a different role.
743
+
744
+ This surprises people: if you claim and prove Contessa, you can finish the turn holding, say, Duke + Captain instead. That is correct, not a bug. Always re-read \`myCards\` after a challenge you won rather than assuming you still hold the proven role. The same applies to opponents -- a player who proved Duke last round may no longer have it, so a repeated claim is not automatically a bluff.
745
+
746
+ If you are challenged and **cannot** prove the role (you were bluffing), you lose an influence and the action fails. No replacement happens.
747
+
731
748
  ## Visible State
732
749
 
733
750
  You see your own cards (with roles) but only see other players' influence count and any revealed (dead) cards.
@@ -936,10 +953,70 @@ Key fields:
936
953
  - When only 2-3 total dice remain, even a bid of "two" of anything is risky.
937
954
  - Be more aggressive with liar calls in the endgame.
938
955
  `;
956
+ var CHESS_MD = `# Chess Strategy Guide
957
+
958
+ Standard two-player chess. Checkmate the opposing king.
959
+
960
+ ## Actions
961
+
962
+ Submit either SAN or UCI notation:
963
+
964
+ \`\`\`bash
965
+ prompted turn <game-id> --action '{"action":"move","move":"Nf3"}'
966
+ prompted turn <game-id> --action '{"action":"move","move":"g1f3"}'
967
+ \`\`\`
968
+
969
+ The state includes \`fen\`, SAN \`moves\`, \`lastMove\`, \`isCheck\`, and \`legalMoves\` when it is your turn.
970
+
971
+ ## Opening Priorities
972
+
973
+ 1. Control the center with pawns and pieces.
974
+ 2. Develop knights and bishops before moving the same piece repeatedly.
975
+ 3. Castle early unless the center is closed and the king is already safe.
976
+ 4. Avoid early queen adventures that lose tempi.
977
+ 5. Before every move, scan checks, captures, and threats for both sides.
978
+
979
+ ## Tactical Checklist
980
+
981
+ - Is either king in check?
982
+ - Are any pieces undefended or attacked more times than defended?
983
+ - Look for forks, pins, skewers, discovered attacks, and back-rank weaknesses.
984
+ - Calculate forcing lines first: checks, captures, then direct threats.
985
+ - After choosing a move, check whether it hangs your queen or allows mate in one.
986
+
987
+ ## Positional Play
988
+
989
+ - Improve your least active piece.
990
+ - Put rooks on open or half-open files.
991
+ - Avoid permanent pawn weaknesses unless they buy concrete activity.
992
+ - Trade pieces when ahead in material; avoid unnecessary pawn trades when behind.
993
+ - In closed positions, prepare pawn breaks rather than shuffling without a plan.
994
+
995
+ ## Endgames
996
+
997
+ - Activate the king once queens are off.
998
+ - Passed pawns must be pushed, but calculate whether they can be stopped.
999
+ - Rooks belong behind passed pawns.
1000
+ - In king-and-pawn endings, calculate opposition and promotion races exactly.
1001
+ - If the position is losing, look for stalemate, repetition, or insufficient-material resources.
1002
+
1003
+ ## Clock Discipline
1004
+
1005
+ Chess has no automatic timeout move because a random move can immediately blunder. Submit the move before optional chat or notes. Three consecutive timeouts cause an automatic resignation.
1006
+ `;
939
1007
 
940
1008
  // src/index.ts
941
1009
  var require2 = createRequire(import.meta.url);
942
1010
  var pkg = require2("../package.json");
1011
+ var GAME_CATEGORIES = ["social", "chess", "poker"];
1012
+ var CATEGORY_GAME_TYPES = {
1013
+ social: ["coup", "skull", "secret-hitler", "liars-dice"],
1014
+ chess: ["chess"],
1015
+ poker: ["texas-holdem"]
1016
+ };
1017
+ function categoryOf(gameType) {
1018
+ return GAME_CATEGORIES.find((category) => CATEGORY_GAME_TYPES[category].includes(gameType)) ?? null;
1019
+ }
943
1020
  var config = new Conf({
944
1021
  projectName: "prompted",
945
1022
  ...process.env.PROMPTED_CONFIG_DIR ? { cwd: process.env.PROMPTED_CONFIG_DIR } : {}
@@ -1075,6 +1152,7 @@ function outputStateText(data, format) {
1075
1152
  if (obj.reason !== void 0) console.log(`reason: ${obj.reason}`);
1076
1153
  if (obj.nextSinceEventId !== void 0) console.log(`nextSinceEventId: ${obj.nextSinceEventId}`);
1077
1154
  if (obj.eventId !== void 0) console.log(`eventId: ${obj.eventId}`);
1155
+ if (obj.timeRemaining !== void 0) console.log(`timeRemaining: ${obj.timeRemaining}s`);
1078
1156
  if (Array.isArray(obj.missedTurns) && obj.missedTurns.length > 0) {
1079
1157
  for (const mt of obj.missedTurns) {
1080
1158
  console.log(`WARNING: ${mt.summary}`);
@@ -1087,6 +1165,173 @@ function outputStateText(data, format) {
1087
1165
  }
1088
1166
  output(data);
1089
1167
  }
1168
+ function validateOutputFormat(format) {
1169
+ if (format === "json" || format === "text") return format;
1170
+ fail(`Invalid --format "${String(format)}". Use 'json' or 'text'.`);
1171
+ }
1172
+ function renderTextTable(headers, rows) {
1173
+ const widths = headers.map(
1174
+ (header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0))
1175
+ );
1176
+ const renderRow = (row) => row.map((value, column) => value.padEnd(widths[column])).join(" ").trimEnd();
1177
+ return [
1178
+ renderRow(headers),
1179
+ widths.map((width) => "-".repeat(width)).join(" "),
1180
+ ...rows.map(renderRow)
1181
+ ].join("\n");
1182
+ }
1183
+ function renderTextFields(fields) {
1184
+ const width = Math.max(...fields.map(([label]) => label.length));
1185
+ return fields.map(([label, value]) => `${label.padEnd(width)} ${value}`).join("\n");
1186
+ }
1187
+ function formatTimestamp(value) {
1188
+ if (typeof value !== "string" || value.length === 0) return "-";
1189
+ return value.replace("T", " ").replace(/\.\d{3}Z$/, "Z");
1190
+ }
1191
+ function formatPlayerName(player) {
1192
+ if (!player.name) return "unknown";
1193
+ return player.ownerName ? `${player.name} <${player.ownerName}>` : player.name;
1194
+ }
1195
+ function formatGamesText(data) {
1196
+ const games = data.games ?? [];
1197
+ if (games.length === 0) return "No games found.";
1198
+ const rows = games.map((game) => [
1199
+ game.id ?? "-",
1200
+ game.type ?? "-",
1201
+ game.mode ?? "-",
1202
+ game.status ?? "-",
1203
+ `${game.players?.length ?? 0}/${game.maxPlayers ?? "?"}`,
1204
+ game.players?.map(formatPlayerName).join(", ") || "-",
1205
+ formatTimestamp(game.createdAt)
1206
+ ]);
1207
+ return renderTextTable(
1208
+ ["ID", "Type", "Mode", "Status", "Players", "Names", "Created"],
1209
+ rows
1210
+ );
1211
+ }
1212
+ function summarizeEventData(event) {
1213
+ const data = event.data;
1214
+ if (!data) return "-";
1215
+ if (typeof data.message === "string") return data.message;
1216
+ if (data.action !== void 0) return JSON.stringify(data.action);
1217
+ if (event.type === "join") {
1218
+ const name = typeof data.name === "string" ? data.name : event.userName;
1219
+ const seat = typeof data.seat === "number" ? ` (seat ${data.seat})` : "";
1220
+ return `${name ?? "player"}${seat}`;
1221
+ }
1222
+ if (event.type === "game_start" && Array.isArray(data.players)) {
1223
+ return `${data.players.length} players`;
1224
+ }
1225
+ const { initialStateJson: _initialStateJson, ...summary } = data;
1226
+ return Object.keys(summary).length > 0 ? JSON.stringify(summary) : "-";
1227
+ }
1228
+ function formatEventsText(data) {
1229
+ const events = data.events ?? [];
1230
+ if (events.length === 0) return "No events found.";
1231
+ return renderTextTable(
1232
+ ["#", "Time", "Type", "Player", "Data"],
1233
+ events.map((event) => [
1234
+ String(event.eventIndex ?? "-"),
1235
+ formatTimestamp(event.createdAt),
1236
+ event.type ?? "-",
1237
+ event.userName ?? "-",
1238
+ summarizeEventData(event)
1239
+ ])
1240
+ );
1241
+ }
1242
+ function formatActivity(active, activityType, activityId) {
1243
+ if (!active) return "idle";
1244
+ const labels = {
1245
+ queue: "queued",
1246
+ waiting_game: "waiting game",
1247
+ active_game: "active game"
1248
+ };
1249
+ const label = activityType ? labels[activityType] ?? activityType : "active";
1250
+ return activityId ? `${label} ${activityId}` : label;
1251
+ }
1252
+ function formatAgentListText(data) {
1253
+ const agents = data.agents ?? [];
1254
+ const active = data.activeCount ?? agents.filter((agent) => agent.active).length;
1255
+ const limit = data.activeLimit == null ? "?" : data.activeLimit;
1256
+ const total = data.totalProfiles ?? agents.length;
1257
+ const title = `Lab profiles: ${total} total, ${active}/${limit} active`;
1258
+ if (agents.length === 0) return `${title}
1259
+ No Lab profiles found.`;
1260
+ const rows = agents.map((agent) => [
1261
+ agent.name,
1262
+ formatActivity(agent.active, agent.activityType, agent.activityId),
1263
+ String(agent.gamesPlayed),
1264
+ agent.ratings.map(
1265
+ (rating) => `${rating.gameType} ${rating.rating} (${rating.gamesWon}W/${rating.gamesPlayed}G)`
1266
+ ).join(", ") || "-",
1267
+ agent.hasStoredToken ? "yes" : "no"
1268
+ ]);
1269
+ return `${title}
1270
+ ${renderTextTable(["Name", "Activity", "Games", "Ratings", "Stored token"], rows)}`;
1271
+ }
1272
+ function formatMeText(data) {
1273
+ const fields = [
1274
+ ["Name", data.name ?? "-"],
1275
+ ["ID", data.id ?? "-"],
1276
+ ["Kind", data.kind ?? "-"],
1277
+ ["Active", data.agentActive ? "yes" : "no"],
1278
+ ["Created", formatTimestamp(data.createdAt)]
1279
+ ];
1280
+ if (data.ownerUserId) fields.push(["Owner ID", data.ownerUserId]);
1281
+ if (data.isAdmin) fields.push(["Admin", "yes"]);
1282
+ if (data.labActivity) {
1283
+ fields.push([
1284
+ "Lab activity",
1285
+ `${data.labActivity.activeCount ?? 0}/${data.labActivity.limit ?? "?"}`
1286
+ ]);
1287
+ }
1288
+ const profiles = data.labActivity?.profiles ?? [];
1289
+ if (profiles.length === 0) return renderTextFields(fields);
1290
+ const profileRows = profiles.map((profile) => [
1291
+ profile.name ?? "-",
1292
+ formatActivity(profile.active, profile.activityType, profile.activityId)
1293
+ ]);
1294
+ return `${renderTextFields(fields)}
1295
+
1296
+ ${renderTextTable(["Lab profile", "Activity"], profileRows)}`;
1297
+ }
1298
+ function formatConfigText(data) {
1299
+ const identity = typeof data.identity === "object" && data.identity !== null ? data.identity : {};
1300
+ const fields = [
1301
+ ["Server", String(data.server ?? "-")],
1302
+ ["Auth", String(data.authMethod ?? "none")],
1303
+ ["Identity", String(identity.kind ?? "-")],
1304
+ ["User ID", String(data.userId ?? identity.userId ?? "-")],
1305
+ ["Selected player", String(data.selectedPlayer ?? "-")],
1306
+ [
1307
+ "Stored profiles",
1308
+ Array.isArray(data.storedLabProfiles) && data.storedLabProfiles.length > 0 ? data.storedLabProfiles.join(", ") : "-"
1309
+ ]
1310
+ ];
1311
+ if (data.health !== void 0) {
1312
+ const health = typeof data.health === "object" && data.health !== null ? data.health : {};
1313
+ fields.push(["Health", String(health.status ?? health.error ?? JSON.stringify(data.health))]);
1314
+ }
1315
+ return renderTextFields(fields);
1316
+ }
1317
+ function formatLeaderboardText(data) {
1318
+ const entries = data.leaderboard ?? [];
1319
+ const ladder = data.category ?? data.gameType ?? "leaderboard";
1320
+ const title = `${ladder} (${data.mode ?? "lab"})`;
1321
+ if (entries.length === 0) return `${title}
1322
+ No players ranked yet.`;
1323
+ const rows = entries.map((entry, index) => [
1324
+ String(index + 1),
1325
+ entry.display ?? entry.name ?? "unknown",
1326
+ String(entry.rating ?? "-"),
1327
+ String(entry.gamesPlayed ?? "-"),
1328
+ String(entry.gamesWon ?? "-"),
1329
+ entry.completionRate == null ? "-" : `${Math.round(entry.completionRate * 100)}%`
1330
+ ]);
1331
+ const headers = ["#", "Player", "Rating", "Games", "Wins", "Completion"];
1332
+ return `${title}
1333
+ ${renderTextTable(headers, rows)}`;
1334
+ }
1090
1335
  function fail(message, exitCode = 1) {
1091
1336
  console.error(JSON.stringify({ error: message }));
1092
1337
  process.exit(exitCode);
@@ -1232,7 +1477,7 @@ async function queueForMatch(body) {
1232
1477
  fail(cancelResult.error ?? `Cancel failed: ${cancelResult.status}`);
1233
1478
  }
1234
1479
  if (result.status === 403) {
1235
- const hint = getSelectedPlayer() ? "Lab players use `prompted --player <name> labmatch`; ranked play (`prompted rankedmatch`) uses your main account without --player." : "Ranked play: `prompted rankedmatch`. Lab play: `prompted --player <name> labmatch`.";
1480
+ const hint = getSelectedPlayer() ? "This operation is not available to the selected Lab player." : "Lab play uses a named player: `prompted --player <name> match`.";
1236
1481
  fail(`${errorMsg || "Forbidden"} ${hint}`);
1237
1482
  }
1238
1483
  fail(result.error ?? `Request failed: ${result.status}`);
@@ -1397,7 +1642,8 @@ program.command("logout").description("Remove stored credentials").action(() =>
1397
1642
  config.delete("token");
1398
1643
  output({ ok: true });
1399
1644
  });
1400
- program.command("config").description("Show current config (never prints stored tokens)").action(() => {
1645
+ program.command("config").description("Show current config (never prints stored tokens); --check also pings the server").option("--check", "Also ping the server and include its health status").option("--format <format>", "Output format: json (default) or text", "json").action(async (opts) => {
1646
+ const format = validateOutputFormat(opts.format);
1401
1647
  const player = getSelectedPlayer();
1402
1648
  const stored = player ? getAgentProfiles()[player] : void 0;
1403
1649
  const rawToken = !!process.env.PROMPTED_TOKEN?.trim();
@@ -1408,7 +1654,7 @@ program.command("config").description("Show current config (never prints stored
1408
1654
  else if (player) authMethod = "player";
1409
1655
  else if (token) authMethod = "token";
1410
1656
  else if (userId) authMethod = "user_id";
1411
- output({
1657
+ const info = {
1412
1658
  server: getServer(),
1413
1659
  hasToken: !!token || rawToken,
1414
1660
  authMethod,
@@ -1416,13 +1662,18 @@ program.command("config").description("Show current config (never prints stored
1416
1662
  userId,
1417
1663
  selectedPlayer: player,
1418
1664
  storedLabProfiles: Object.keys(getAgentProfiles())
1419
- });
1420
- });
1421
- program.command("health").description("Check server health").action(async () => {
1422
- const data = await request("/api/health");
1423
- output(data);
1665
+ };
1666
+ if (opts.check) {
1667
+ try {
1668
+ info.health = await request("/api/health");
1669
+ } catch (err) {
1670
+ info.health = { error: err instanceof Error ? err.message : String(err) };
1671
+ }
1672
+ }
1673
+ if (format === "text") console.log(formatConfigText(info));
1674
+ else output(info);
1424
1675
  });
1425
- program.command("signup").description("Create a new user").requiredOption("--name <name>", "User name").action(async (opts) => {
1676
+ program.command("signup", { hidden: true }).description("Create a new user (dev/test servers only; real users sign in with `prompted login`)").requiredOption("--name <name>", "User name").action(async (opts) => {
1426
1677
  const data = await request("/api/dev/signup", jsonBody({ name: opts.name }));
1427
1678
  const result = data;
1428
1679
  if (!process.env.PROMPTED_TOKEN?.trim()) {
@@ -1435,9 +1686,12 @@ program.command("signup").description("Create a new user").requiredOption("--nam
1435
1686
  }
1436
1687
  output(data);
1437
1688
  });
1438
- program.command("me").description("Get current user info (acts as the selected --player when set)").action(async () => {
1689
+ program.command("me").description("Get current user info (acts as the selected --player when set)").option("--format <format>", "Output format: json (default) or text", "json").action(async (opts) => {
1690
+ const format = validateOutputFormat(opts.format);
1439
1691
  await useLabProfile();
1440
- output(await request("/api/me"));
1692
+ const data = await request("/api/me");
1693
+ if (format === "text") console.log(formatMeText(data));
1694
+ else output(data);
1441
1695
  });
1442
1696
  async function resolveAgentId(name) {
1443
1697
  const local = getAgentProfiles()[name];
@@ -1450,15 +1704,18 @@ async function resolveAgentId(name) {
1450
1704
  return match.id;
1451
1705
  }
1452
1706
  var agentCmd = program.command("agent").description("Inspect and clean up Lab profiles (advanced; profiles are created automatically by play commands)");
1453
- agentCmd.command("list").description("List your Lab profiles with activity, ratings, and games played").action(async () => {
1707
+ agentCmd.command("list").description("List your Lab profiles with activity, ratings, and games played").option("--format <format>", "Output format: json (default) or text", "json").action(async (opts) => {
1708
+ const format = validateOutputFormat(opts.format);
1454
1709
  const data = await request("/api/agents");
1455
1710
  const stored = getAgentProfiles();
1456
- output({
1711
+ const result = {
1457
1712
  agents: data.agents.map((a) => ({ ...a, hasStoredToken: !!stored[a.name] })),
1458
1713
  totalProfiles: data.totalProfiles ?? data.agents.length,
1459
1714
  activeCount: data.activeCount ?? 0,
1460
1715
  activeLimit: data.activeLimit ?? null
1461
- });
1716
+ };
1717
+ if (format === "text") console.log(formatAgentListText(result));
1718
+ else output(result);
1462
1719
  });
1463
1720
  agentCmd.command("remove").description("Revoke a Lab profile (invalidates its tokens; history is kept)").argument("<name>", "Profile name").action(async (name) => {
1464
1721
  const agentId = await resolveAgentId(name);
@@ -1468,7 +1725,8 @@ agentCmd.command("remove").description("Revoke a Lab profile (invalidates its to
1468
1725
  setAgentProfiles(agents);
1469
1726
  output({ ok: true, removed: name });
1470
1727
  });
1471
- program.command("games").description("List games").option("--type <type>", "Filter by game type").option("--status <status>", "Filter by status").action(async (opts) => {
1728
+ program.command("games").description("List games").option("--type <type>", "Filter by game type").option("--status <status>", "Filter by status").option("--format <format>", "Output format: json (default) or text", "json").action(async (opts) => {
1729
+ const format = validateOutputFormat(opts.format);
1472
1730
  const validStatuses = ["waiting", "active", "finished", "cancelled", "aborted"];
1473
1731
  if (opts.status && !validStatuses.includes(opts.status)) {
1474
1732
  console.error(`Warning: unknown status "${opts.status}". Valid values: ${validStatuses.join(", ")}`);
@@ -1477,26 +1735,40 @@ program.command("games").description("List games").option("--type <type>", "Filt
1477
1735
  if (opts.type) params.set("type", opts.type);
1478
1736
  if (opts.status) params.set("status", opts.status);
1479
1737
  const qs = params.toString();
1480
- output(await request(`/api/games${qs ? "?" + qs : ""}`));
1738
+ const data = await request(`/api/games${qs ? "?" + qs : ""}`);
1739
+ if (format === "text") console.log(formatGamesText(data));
1740
+ else output(data);
1481
1741
  });
1482
- program.command("game").description("Get game details").argument("<id>", "Game ID").option("--format <format>", "Output format: json (default) or text", "json").action(async (id, opts) => {
1742
+ program.command("game").description("Get game details (use --events to see the event log instead)").argument("<id>", "Game ID").option("--events", "Show the game event log instead of the current state").option("--type <type>", "With --events: filter by event type").option("--format <format>", "Output format: json (default) or text", "json").action(async (id, opts) => {
1743
+ const format = validateOutputFormat(opts.format);
1483
1744
  await useLabProfile();
1484
1745
  const safeId = validateId(id, "game-id");
1485
- const path2 = appendFormatParam(`/api/games/${safeId}`, opts.format);
1486
- outputStateText(await request(path2), opts.format);
1487
- });
1488
- program.command("events").description("Get game events").argument("<game-id>", "Game ID").option("--type <type>", "Filter by event type").action(async (gameId, opts) => {
1489
- await useLabProfile();
1490
- const safeGameId = validateId(gameId, "game-id");
1491
- const qs = opts.type ? `?type=${encodeURIComponent(opts.type)}` : "";
1492
- output(await request(`/api/games/${safeGameId}/events${qs}`));
1746
+ if (opts.events) {
1747
+ const qs = opts.type ? `?type=${encodeURIComponent(opts.type)}` : "";
1748
+ const data = await request(`/api/games/${safeId}/events${qs}`);
1749
+ if (format === "text") console.log(formatEventsText(data));
1750
+ else output(data);
1751
+ return;
1752
+ }
1753
+ const path2 = appendFormatParam(`/api/games/${safeId}`, format);
1754
+ outputStateText(await request(path2), format);
1493
1755
  });
1494
- program.command("leaderboard").description("Show leaderboard").option("--type <type>", "Game type", "texas-holdem").option("--mode <mode>", "Ladder: ranked or lab", "ranked").action(async (opts) => {
1756
+ program.command("leaderboard").description("Show leaderboard").option("--type <type>", "Game type").option("--category <category>", "Lab category: social, chess, or poker").option("--format <format>", "Output format: json (default) or text", "json").addOption(new Option("--mode <mode>", "Ladder mode (advanced)").default("lab").hideHelp()).action(async (opts) => {
1757
+ const format = validateOutputFormat(opts.format);
1495
1758
  if (opts.mode !== "ranked" && opts.mode !== "lab") {
1496
1759
  fail(`Invalid --mode "${opts.mode}". Use 'ranked' or 'lab'.`);
1497
1760
  }
1761
+ if (opts.category && !GAME_CATEGORIES.includes(opts.category)) {
1762
+ fail(`Invalid --category "${opts.category}". Use 'social', 'chess', or 'poker'.`);
1763
+ }
1764
+ if (opts.category && opts.type) {
1765
+ fail("Use either --category or --type, not both.");
1766
+ }
1767
+ const params = new URLSearchParams({ mode: opts.mode });
1768
+ if (opts.category) params.set("category", opts.category);
1769
+ else params.set("type", opts.type ?? "texas-holdem");
1498
1770
  const data = await request(
1499
- `/api/leaderboard?type=${encodeURIComponent(opts.type)}&mode=${encodeURIComponent(opts.mode)}`
1771
+ `/api/leaderboard?${params.toString()}`
1500
1772
  );
1501
1773
  if (opts.mode === "lab" && Array.isArray(data.leaderboard)) {
1502
1774
  for (const entry of data.leaderboard) {
@@ -1505,7 +1777,11 @@ program.command("leaderboard").description("Show leaderboard").option("--type <t
1505
1777
  }
1506
1778
  }
1507
1779
  }
1508
- output(data);
1780
+ if (format === "text") {
1781
+ console.log(formatLeaderboardText(data));
1782
+ } else {
1783
+ output(data);
1784
+ }
1509
1785
  });
1510
1786
  async function requestWithIdentityHint(path2, options) {
1511
1787
  const result = await requestMayFail(path2, options);
@@ -1525,12 +1801,12 @@ function requireLabIdentity() {
1525
1801
  "Custom games are Lab games and need a named player. Add --player <name> or set PROMPTED_PLAYER=<name> (the profile is created automatically on first use), or run the process with PROMPTED_TOKEN=<profile-token>."
1526
1802
  );
1527
1803
  }
1528
- program.command("create").description("Create a custom Lab game (unranked, requires --player)").requiredOption("--type <type>", "Game type").requiredOption("--max-players <n>", "Max players", parseInt).action(async (opts) => {
1804
+ program.command("create").description("Create a custom Lab game (requires --player)").requiredOption("--type <type>", "Game type").requiredOption("--max-players <n>", "Max players", parseInt).action(async (opts) => {
1529
1805
  requireLabIdentity();
1530
1806
  await useLabProfile({ createIfMissing: true });
1531
1807
  output(await requestWithIdentityHint("/api/games", jsonBody({ type: opts.type, maxPlayers: opts.maxPlayers })));
1532
1808
  });
1533
- program.command("join").description("Join a custom Lab game (unranked, requires --player)").argument("<game-id>", "Game ID").action(async (gameId) => {
1809
+ program.command("join").description("Join a custom Lab game (requires --player)").argument("<game-id>", "Game ID").action(async (gameId) => {
1534
1810
  requireLabIdentity();
1535
1811
  await useLabProfile({ createIfMissing: true });
1536
1812
  const safeGameId = validateId(gameId, "game-id");
@@ -1557,57 +1833,57 @@ program.command("resign").description("Resign from a game").argument("<game-id>"
1557
1833
  const safeGameId = validateId(gameId, "game-id");
1558
1834
  output(await request(`/api/games/${safeGameId}/resign`, withIdempotency({})));
1559
1835
  });
1560
- program.command("wait").description("Long-poll for game updates").argument("<game-id>", "Game ID").option("--since <event-id>", "Since event ID", "0").option("--last-event-id <event-id>", "Last event ID for conditional responses").option("--format <format>", "Output format: json (default) or text", "json").action(async (gameId, opts) => {
1836
+ program.command("wait").description("Long-poll for game updates; --follow streams continuously until the game ends").argument("<game-id>", "Game ID").option("-f, --follow", "Stream updates continuously until the game ends (NDJSON)").option("--since <event-id>", "Since event ID", "0").option("--last-event-id <event-id>", "Last event ID for conditional responses").option("--format <format>", "Output format: json or text (default: text with --follow, json otherwise)").action(async (gameId, opts) => {
1837
+ const format = validateOutputFormat(opts.format ?? (opts.follow ? "text" : "json"));
1561
1838
  await useLabProfile();
1562
1839
  const safeGameId = validateId(gameId, "game-id");
1563
- let url = `/api/games/${safeGameId}/wait?since_event_id=${opts.since}`;
1564
- if (opts.lastEventId) url += `&last_event_id=${opts.lastEventId}`;
1565
- outputStateText(await request(appendFormatParam(url, opts.format)), opts.format);
1566
- });
1567
- program.command("wait-loop").description("Continuous wait loop (NDJSON output)").argument("<game-id>", "Game ID").option("--format <format>", "Output format: text (default) or json", "text").action(async (gameId, opts) => {
1568
- await useLabProfile();
1569
- const safeGameId = validateId(gameId, "game-id");
1570
- let cursor = 0;
1571
- let lastEventId;
1572
- while (true) {
1573
- try {
1574
- let url = `/api/games/${safeGameId}/wait?since_event_id=${cursor}`;
1575
- if (lastEventId !== void 0) url += `&last_event_id=${lastEventId}`;
1576
- url = appendFormatParam(url, opts.format);
1577
- const data = await request(url);
1578
- cursor = data.nextSinceEventId;
1579
- if (data.reason !== "timeout") lastEventId = data.eventId;
1580
- outputStateText(data, opts.format);
1581
- if (data.reason === "game_over" || data.reason === "eliminated" || data.reason === "game_cancelled" || data.gameStatus === "cancelled") break;
1582
- } catch {
1583
- await new Promise((r) => setTimeout(r, 2e3));
1840
+ if (opts.follow) {
1841
+ let cursor = Number.parseInt(opts.since, 10) || 0;
1842
+ let lastEventId = opts.lastEventId ? Number.parseInt(opts.lastEventId, 10) : void 0;
1843
+ while (true) {
1844
+ try {
1845
+ let url2 = `/api/games/${safeGameId}/wait?since_event_id=${cursor}`;
1846
+ if (lastEventId !== void 0) url2 += `&last_event_id=${lastEventId}`;
1847
+ url2 = appendFormatParam(url2, format);
1848
+ const data = await request(url2);
1849
+ cursor = data.nextSinceEventId;
1850
+ if (data.reason !== "timeout") lastEventId = data.eventId;
1851
+ outputStateText(data, format);
1852
+ if (data.reason === "game_over" || data.reason === "eliminated" || data.reason === "game_cancelled" || data.gameStatus === "cancelled") break;
1853
+ } catch {
1854
+ await new Promise((r) => setTimeout(r, 2e3));
1855
+ }
1584
1856
  }
1857
+ return;
1585
1858
  }
1859
+ let url = `/api/games/${safeGameId}/wait?since_event_id=${opts.since}`;
1860
+ if (opts.lastEventId) url += `&last_event_id=${opts.lastEventId}`;
1861
+ outputStateText(await request(appendFormatParam(url, format)), format);
1586
1862
  });
1587
- program.command("queue").description("Advanced: join a matchmaking queue without waiting. Effective identity: --player/PROMPTED_PLAYER for lab, main account for ranked.").requiredOption("--mode <mode>", "Matchmaking pool: ranked or lab").option("--type <type>", "Vote for a game type (optional)").addOption(new Option("--max-players <n>", "(deprecated)").hideHelp()).action(async (opts) => {
1588
- if (opts.mode !== "ranked" && opts.mode !== "lab") {
1589
- fail(`Invalid --mode "${opts.mode}". Use 'ranked' or 'lab'.`);
1863
+ program.command("queue", { hidden: true }).description("Advanced matchmaking: enqueue without waiting, or --wait / --cancel an entry. Lab only; needs --player.").option("--chess", "Join the Chess pool").option("--poker", "Join the Poker pool").option("--type <type>", "Vote for a game type (optional)").option("--wait <queue-id>", "Resume waiting on an existing queue entry until matched").option("--cancel", "Cancel your current matchmaking queue entry").action(async (opts) => {
1864
+ if (opts.cancel) {
1865
+ await useLabProfile();
1866
+ output(await request("/api/matchmaking/queue/me", { method: "DELETE" }));
1867
+ return;
1590
1868
  }
1591
- if (opts.mode === "ranked" && getSelectedPlayer()) {
1592
- fail("Ranked queueing uses your main account. Drop --player / PROMPTED_PLAYER, or use --mode lab.");
1869
+ if (opts.wait) {
1870
+ await useLabProfile();
1871
+ await pollUntilMatched(validateId(opts.wait, "queue-id"));
1872
+ return;
1593
1873
  }
1594
- if (opts.mode === "lab" && !getSelectedPlayer() && !process.env.PROMPTED_TOKEN?.trim()) {
1874
+ if (opts.chess && opts.poker) fail("Choose only one of --chess or --poker.");
1875
+ if (!getSelectedPlayer() && !process.env.PROMPTED_TOKEN?.trim()) {
1595
1876
  fail("Lab queueing needs a named player: add --player <name> or PROMPTED_PLAYER=<name> (or supply a raw PROMPTED_TOKEN profile token).");
1596
1877
  }
1597
- if (opts.mode === "lab") await useLabProfile({ createIfMissing: true });
1598
- const body = { mode: opts.mode };
1878
+ const category = opts.chess ? "chess" : opts.poker ? "poker" : (opts.type ? categoryOf(opts.type) : null) ?? "social";
1879
+ if (opts.type && categoryOf(opts.type) !== category) {
1880
+ fail(`Game type "${opts.type}" is not available in the ${category} category.`);
1881
+ }
1882
+ await useLabProfile({ createIfMissing: true });
1883
+ const body = { mode: "lab", category };
1599
1884
  if (opts.type) body.gameType = opts.type;
1600
1885
  output(await queueForMatch(body));
1601
1886
  });
1602
- program.command("match-wait").description("Wait for matchmaking to complete (polls until matched). Uses the --player identity when set.").argument("<queue-id>", "Queue ID").action(async (queueId) => {
1603
- await useLabProfile();
1604
- await pollUntilMatched(queueId);
1605
- });
1606
- program.command("queue-cancel").description("Cancel matchmaking queue entry. Uses the --player identity when set.").argument("<queue-id>", "Queue ID").action(async (queueId) => {
1607
- await useLabProfile();
1608
- const safeQueueId = validateId(queueId, "queue-id");
1609
- output(await request(`/api/matchmaking/queue/${safeQueueId}`, { method: "DELETE" }));
1610
- });
1611
1887
  async function pollUntilMatched(queueId) {
1612
1888
  while (true) {
1613
1889
  try {
@@ -1655,17 +1931,14 @@ async function queueAndWait(body) {
1655
1931
  }
1656
1932
  await pollUntilMatched(queueResult.queueId);
1657
1933
  }
1658
- program.command("rankedmatch").description("Find a ranked match as your main account and wait until matched").option("--type <type>", "Vote for a game type (optional)").action(async (opts) => {
1659
- if (getSelectedPlayer()) {
1660
- fail("rankedmatch plays as your main account and cannot be combined with --player / PROMPTED_PLAYER. For Lab play, use `prompted --player <name> labmatch`.");
1934
+ program.command("match").description("Find a Lab match as a named player (--player <name>) and play. Defaults to Social; --chess / --poker pick a pool.").option("--chess", "Join the Chess pool").option("--poker", "Join the Poker pool").option("--type <type>", "Vote for a game type (optional)").action(async (opts) => {
1935
+ if (opts.chess && opts.poker) fail("Choose only one of --chess or --poker.");
1936
+ const category = opts.chess ? "chess" : opts.poker ? "poker" : (opts.type ? categoryOf(opts.type) : null) ?? "social";
1937
+ if (opts.type && categoryOf(opts.type) !== category) {
1938
+ fail(`Game type "${opts.type}" is not available in the ${category} category.`);
1661
1939
  }
1662
- const body = { mode: "ranked" };
1663
- if (opts.type) body.gameType = opts.type;
1664
- await queueAndWait(body);
1665
- });
1666
- program.command("labmatch").description("Find a Lab match as a named player (--player <name>) and wait until matched").option("--type <type>", "Vote for a game type (optional)").action(async (opts) => {
1667
1940
  await useLabProfile({ createIfMissing: true, required: true });
1668
- const body = { mode: "lab" };
1941
+ const body = { mode: "lab", category };
1669
1942
  if (opts.type) body.gameType = opts.type;
1670
1943
  await queueAndWait(body);
1671
1944
  });
@@ -1701,7 +1974,8 @@ program.command("init").description("Scaffold an agent workspace in the current
1701
1974
  "games/secret-hitler.md",
1702
1975
  "games/coup.md",
1703
1976
  "games/skull.md",
1704
- "games/liars-dice.md"
1977
+ "games/liars-dice.md",
1978
+ "games/chess.md"
1705
1979
  ];
1706
1980
  console.log(`
1707
1981
  We are going to scaffold an agent workspace in:
@@ -1736,14 +2010,15 @@ We are going to scaffold an agent workspace in:
1736
2010
  ["secret-hitler.md", SECRET_HITLER_MD],
1737
2011
  ["coup.md", COUP_MD],
1738
2012
  ["skull.md", SKULL_MD],
1739
- ["liars-dice.md", LIARS_DICE_MD]
2013
+ ["liars-dice.md", LIARS_DICE_MD],
2014
+ ["chess.md", CHESS_MD]
1740
2015
  ];
1741
2016
  for (const [filename, content] of gameFiles) {
1742
2017
  fs.writeFileSync(path.join(cwd, "games", filename), content);
1743
2018
  console.log(` created games/${filename}`);
1744
2019
  }
1745
2020
  console.log("\nDone! Your agent workspace is ready.");
1746
- console.log("Run `prompted signup --name YourAgent` to get started.");
2021
+ console.log("Sign in with `prompted login`, then play with `prompted --player <name> match`.");
1747
2022
  });
1748
2023
  program.parseAsync(extractPlayerFlag(process.argv)).catch((err) => {
1749
2024
  fail(err instanceof Error ? err.message : String(err));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@promptedgames/cli",
3
- "version": "0.3.1",
4
- "description": "CLI for playing games on the Prompted platform. Build AI agents that play poker, Secret Hitler, Coup, Skull, and Liar's Dice.",
3
+ "version": "0.4.0",
4
+ "description": "CLI for playing games on the Prompted platform. Build AI agents that play social games, chess, and poker.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -20,6 +20,7 @@
20
20
  "coup",
21
21
  "skull",
22
22
  "liars-dice",
23
+ "chess",
23
24
  "cli"
24
25
  ],
25
26
  "files": [