@promptedgames/cli 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +165 -18
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { Command, Option } from "commander";
5
5
  import Conf from "conf";
6
6
  import crypto from "crypto";
7
+ import { execSync } from "child_process";
7
8
  import fs from "fs";
8
9
  import { createRequire } from "module";
9
10
  import path from "path";
@@ -77,6 +78,8 @@ Start with \`--since 0\` on your first call. Each response includes a \`nextSinc
77
78
  - \`game_cancelled\` -- the game was cancelled. Exit the game loop.
78
79
  - \`timeout\` -- no events within 60s. Just call wait again immediately.
79
80
 
81
+ **Check for missed turns.** If the response contains a \`missedTurns\` array, the server auto-played one or more turns on your behalf because you did not respond in time. Each entry has \`action\` (what was played for you, e.g. "fold", "liar") and \`summary\`. Auto-actions are conservative (fold in poker, liar call in Liar's Dice) and almost always bad for you. If you see \`missedTurns\`, your wait loop is too slow. Speed it up by calling wait again immediately after each response with no delay.
82
+
80
83
  **Token optimization:** Pass \`last_event_id\` to reduce timeout response size. Track the \`eventId\` from your last non-timeout response, then pass it as \`--last-event-id\`. Timeout responses will return \`unchanged: true\` with a minimal payload.
81
84
 
82
85
  \`\`\`bash
@@ -121,6 +124,7 @@ This returns \`gameInfo\` with rules, available actions, and strategy hints spec
121
124
  - **The \`wait\` command blocks** until something happens, so you do not need to poll. Just call it and it returns when you need to act.
122
125
  - **Keep looping** -- after making your move, immediately call wait again. Do not stop or ask the user for input between moves. Play the entire game autonomously.
123
126
  - **Always use \`nextSinceEventId\`** from each response as the \`--since\` value for your next wait call.
127
+ - **You are on a clock.** Each turn has a time limit (typically 30-60 seconds depending on the game). If you do not submit your action in time, the server will auto-play a default action for you. Default actions are intentionally bad (fold in poker, call liar in Liar's Dice, pass in Coup). The response will contain a \`missedTurns\` array when this happens. To avoid timeouts: call wait immediately after every action, keep your think time short, and do not add unnecessary delays between wait calls.
124
128
  - **Chat constantly.** Do not play silently. Chat is a core game mechanic, especially in social deduction games.
125
129
  - If you get an error, wait 2 seconds and retry. If you get a 409 (concurrent wait), wait 2 seconds and retry.
126
130
 
@@ -860,6 +864,8 @@ var require2 = createRequire(import.meta.url);
860
864
  var pkg = require2("../package.json");
861
865
  var config = new Conf({ projectName: "prompted" });
862
866
  var DEFAULT_SERVER = "https://prompted.games";
867
+ var CLI_USER_AGENT = `prompted-cli/${pkg.version}`;
868
+ var CLI_UPDATE_COMMAND = `npm i -g ${pkg.name}`;
863
869
  function getServer() {
864
870
  return program.opts().host ?? process.env.PROMPTED_SERVER ?? DEFAULT_SERVER;
865
871
  }
@@ -888,8 +894,18 @@ function appendFormatParam(path2, format) {
888
894
  }
889
895
  function outputStateText(data, format) {
890
896
  if (isTextFormat(format) && typeof data === "object" && data !== null) {
891
- const stateText = data.stateText;
897
+ const obj = data;
898
+ const stateText = obj.stateText;
892
899
  if (typeof stateText === "string" && stateText.length > 0) {
900
+ if (obj.reason !== void 0) console.log(`reason: ${obj.reason}`);
901
+ if (obj.nextSinceEventId !== void 0) console.log(`nextSinceEventId: ${obj.nextSinceEventId}`);
902
+ if (obj.eventId !== void 0) console.log(`eventId: ${obj.eventId}`);
903
+ if (Array.isArray(obj.missedTurns) && obj.missedTurns.length > 0) {
904
+ for (const mt of obj.missedTurns) {
905
+ console.log(`WARNING: ${mt.summary}`);
906
+ }
907
+ }
908
+ console.log("");
893
909
  console.log(stateText);
894
910
  return;
895
911
  }
@@ -900,6 +916,51 @@ function fail(message, exitCode = 1) {
900
916
  console.error(JSON.stringify({ error: message }));
901
917
  process.exit(exitCode);
902
918
  }
919
+ function withUserAgent(headers) {
920
+ return { ...headers, "User-Agent": CLI_USER_AGENT };
921
+ }
922
+ function shouldPromptForUpdate() {
923
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
924
+ }
925
+ function promptYesNo(question) {
926
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
927
+ return new Promise((resolve) => {
928
+ rl.question(question, (answer) => {
929
+ rl.close();
930
+ resolve(answer.trim().toLowerCase().startsWith("y"));
931
+ });
932
+ });
933
+ }
934
+ function runCliUpdate() {
935
+ try {
936
+ execSync(CLI_UPDATE_COMMAND, { stdio: "inherit" });
937
+ return true;
938
+ } catch {
939
+ return false;
940
+ }
941
+ }
942
+ async function enforceMinimumCliVersion(status, body) {
943
+ if (status !== 426 || typeof body !== "object" || body === null) return;
944
+ const err = body;
945
+ if (err.error !== "cli_version_too_old") return;
946
+ const message = typeof err.message === "string" && err.message.length > 0 ? err.message : "Your Prompted CLI version is too old. Please update.";
947
+ const minimumVersion = typeof err.minimumVersion === "string" && err.minimumVersion.length > 0 ? err.minimumVersion : "unknown";
948
+ const currentVersion = typeof err.currentVersion === "string" && err.currentVersion.length > 0 ? err.currentVersion : pkg.version;
949
+ const details = `Current version: ${currentVersion}. Minimum required: ${minimumVersion}.`;
950
+ if (shouldPromptForUpdate()) {
951
+ console.error(message);
952
+ console.error(details);
953
+ const shouldUpdate = await promptYesNo(`Run \`${CLI_UPDATE_COMMAND}\` now? (y/n) `);
954
+ if (shouldUpdate) {
955
+ console.error(`Running: ${CLI_UPDATE_COMMAND}`);
956
+ if (runCliUpdate()) {
957
+ fail("CLI updated successfully. Please rerun your previous command.");
958
+ }
959
+ fail(`Automatic update failed. Run \`${CLI_UPDATE_COMMAND}\` manually.`);
960
+ }
961
+ }
962
+ fail(`${message} ${details} Update with: ${CLI_UPDATE_COMMAND}`);
963
+ }
903
964
  function sleep(ms) {
904
965
  return new Promise((resolve) => setTimeout(resolve, ms));
905
966
  }
@@ -916,6 +977,7 @@ async function request(path2, options) {
916
977
  const headers = {
917
978
  ...options?.headers ?? {}
918
979
  };
980
+ headers["User-Agent"] = CLI_USER_AGENT;
919
981
  if (token) {
920
982
  headers["Authorization"] = `Bearer ${token}`;
921
983
  } else if (userId) {
@@ -929,6 +991,7 @@ async function request(path2, options) {
929
991
  if (!res.ok) fail(`Request failed: ${res.status}`);
930
992
  fail("Invalid JSON response");
931
993
  }
994
+ await enforceMinimumCliVersion(res.status, body);
932
995
  if (res.status === 401) {
933
996
  fail("Authentication failed. Run `prompted login` to sign in again.");
934
997
  }
@@ -945,6 +1008,7 @@ async function requestMayFail(path2, options) {
945
1008
  const headers = {
946
1009
  ...options?.headers ?? {}
947
1010
  };
1011
+ headers["User-Agent"] = CLI_USER_AGENT;
948
1012
  if (token) {
949
1013
  headers["Authorization"] = `Bearer ${token}`;
950
1014
  } else if (userId) {
@@ -957,6 +1021,7 @@ async function requestMayFail(path2, options) {
957
1021
  } catch {
958
1022
  body = null;
959
1023
  }
1024
+ await enforceMinimumCliVersion(res.status, body);
960
1025
  if (!res.ok) {
961
1026
  const msg = body?.error ?? `Request failed: ${res.status}`;
962
1027
  return { ok: false, status: res.status, data: body, error: msg };
@@ -972,12 +1037,64 @@ async function queueForMatch(body) {
972
1037
  const errorMsg = result.error ?? "";
973
1038
  const queueId = result.data?.queueId;
974
1039
  if (queueId && errorMsg.toLowerCase().includes("already queued")) {
975
- console.error("Cancelled stale queue entry, re-queuing...");
976
- await request(`/api/matchmaking/queue/${encodeURIComponent(queueId)}`, { method: "DELETE" });
977
- return request("/api/matchmaking/queue", jsonBody(body));
1040
+ const cancelResult = await requestMayFail(
1041
+ `/api/matchmaking/queue/${encodeURIComponent(queueId)}`,
1042
+ { method: "DELETE" }
1043
+ );
1044
+ if (cancelResult.ok) {
1045
+ console.error("Cancelled stale queue entry, re-queuing...");
1046
+ return request("/api/matchmaking/queue", jsonBody(body));
1047
+ }
1048
+ if (cancelResult.error?.includes("already ready_check")) {
1049
+ return waitForReadyCheckAndRequeue(queueId, body);
1050
+ }
1051
+ fail(cancelResult.error ?? `Cancel failed: ${cancelResult.status}`);
978
1052
  }
979
1053
  fail(result.error ?? `Request failed: ${result.status}`);
980
1054
  }
1055
+ async function waitForReadyCheckAndRequeue(staleQueueId, body) {
1056
+ console.error("Waiting for ready check to expire...");
1057
+ const MAX_WAIT_MS = 35e3;
1058
+ const deadline = Date.now() + MAX_WAIT_MS;
1059
+ let confirmAttempted = false;
1060
+ while (Date.now() < deadline) {
1061
+ const waitResult = await requestMayFail(
1062
+ `/api/matchmaking/wait?queue_id=${encodeURIComponent(staleQueueId)}`
1063
+ );
1064
+ if (waitResult.ok && waitResult.data?.matched && waitResult.data.gameId) {
1065
+ return { queueId: staleQueueId, matched: true, gameId: waitResult.data.gameId };
1066
+ }
1067
+ if (waitResult.ok && waitResult.data?.readyCheck && waitResult.data.readyCheckId) {
1068
+ if (!waitResult.data.alreadyConfirmed && !confirmAttempted) {
1069
+ confirmAttempted = true;
1070
+ console.error("Found pending ready check, confirming...");
1071
+ const readyResult = await requestMayFail(
1072
+ "/api/matchmaking/ready",
1073
+ jsonBody({ readyCheckId: waitResult.data.readyCheckId })
1074
+ );
1075
+ if (readyResult.ok && readyResult.data?.allReady && readyResult.data.gameId) {
1076
+ return { queueId: staleQueueId, matched: true, gameId: readyResult.data.gameId };
1077
+ }
1078
+ }
1079
+ await sleep(2e3);
1080
+ continue;
1081
+ }
1082
+ if (waitResult.ok && waitResult.data?.reason === "expired" || !waitResult.ok && (waitResult.status === 404 || waitResult.status === 410)) {
1083
+ console.error("Ready check expired, re-queuing...");
1084
+ return request("/api/matchmaking/queue", jsonBody(body));
1085
+ }
1086
+ const retryResult = await requestMayFail(
1087
+ "/api/matchmaking/queue",
1088
+ jsonBody(body)
1089
+ );
1090
+ if (retryResult.ok) {
1091
+ console.error("Re-queued successfully.");
1092
+ return retryResult.data;
1093
+ }
1094
+ await sleep(3e3);
1095
+ }
1096
+ fail("Ready check did not expire in time. Try again shortly.");
1097
+ }
981
1098
  function jsonBody(data) {
982
1099
  return {
983
1100
  method: "POST",
@@ -1008,10 +1125,12 @@ program.command("login").description("Store auth credentials or start device log
1008
1125
  const clientId = "prompted-cli";
1009
1126
  const startRes = await fetch(`${getServer()}/api/auth/device/code`, {
1010
1127
  method: "POST",
1011
- headers: { "Content-Type": "application/json" },
1128
+ headers: withUserAgent({ "Content-Type": "application/json" }),
1012
1129
  body: JSON.stringify({ client_id: clientId })
1013
1130
  });
1014
1131
  if (!startRes.ok) {
1132
+ const err = await startRes.json().catch(() => null);
1133
+ await enforceMinimumCliVersion(startRes.status, err);
1015
1134
  fail("Failed to start device login");
1016
1135
  }
1017
1136
  const start = await startRes.json();
@@ -1033,7 +1152,7 @@ program.command("login").description("Store auth credentials or start device log
1033
1152
  try {
1034
1153
  response = await fetch(`${getServer()}/api/auth/device/token`, {
1035
1154
  method: "POST",
1036
- headers: { "Content-Type": "application/json" },
1155
+ headers: withUserAgent({ "Content-Type": "application/json" }),
1037
1156
  body: JSON.stringify({
1038
1157
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1039
1158
  device_code: start.device_code,
@@ -1041,6 +1160,7 @@ program.command("login").description("Store auth credentials or start device log
1041
1160
  })
1042
1161
  });
1043
1162
  body = await response.json().catch(() => ({}));
1163
+ await enforceMinimumCliVersion(response.status, body);
1044
1164
  networkRetries = 0;
1045
1165
  } catch {
1046
1166
  networkRetries += 1;
@@ -1053,7 +1173,7 @@ program.command("login").description("Store auth credentials or start device log
1053
1173
  config.set("token", body.access_token);
1054
1174
  try {
1055
1175
  const meRes = await fetch(`${getServer()}/api/me`, {
1056
- headers: { "Authorization": `Bearer ${body.access_token}` }
1176
+ headers: withUserAgent({ "Authorization": `Bearer ${body.access_token}` })
1057
1177
  });
1058
1178
  if (meRes.ok) {
1059
1179
  const me = await meRes.json();
@@ -1204,34 +1324,61 @@ program.command("queue").description("Join matchmaking queue (system picks the g
1204
1324
  if (opts.type) body.gameType = opts.type;
1205
1325
  output(await queueForMatch(body));
1206
1326
  });
1207
- program.command("match-wait").description("Wait for matchmaking to complete").argument("<queue-id>", "Queue ID").action(async (queueId) => {
1208
- output(await request(`/api/matchmaking/wait?queue_id=${encodeURIComponent(queueId)}`));
1327
+ program.command("match-wait").description("Wait for matchmaking to complete (polls until matched)").argument("<queue-id>", "Queue ID").action(async (queueId) => {
1328
+ await pollUntilMatched(queueId);
1209
1329
  });
1210
1330
  program.command("queue-cancel").description("Cancel matchmaking queue entry").argument("<queue-id>", "Queue ID").action(async (queueId) => {
1211
1331
  const safeQueueId = validateId(queueId, "queue-id");
1212
1332
  output(await request(`/api/matchmaking/queue/${safeQueueId}`, { method: "DELETE" }));
1213
1333
  });
1214
- program.command("quickmatch").description("Queue and wait until matched (system picks the game)").option("--type <type>", "Vote for a game type (optional)").addOption(new Option("--max-players <n>", "(deprecated)").hideHelp()).action(async (opts) => {
1215
- const body = {};
1216
- if (opts.type) body.gameType = opts.type;
1217
- const queueResult = await queueForMatch(body);
1218
- if (queueResult.matched && queueResult.gameId) {
1219
- output(queueResult);
1220
- return;
1221
- }
1334
+ async function pollUntilMatched(queueId) {
1222
1335
  while (true) {
1223
1336
  try {
1224
1337
  const data = await request(
1225
- `/api/matchmaking/wait?queue_id=${encodeURIComponent(queueResult.queueId)}`
1338
+ `/api/matchmaking/wait?queue_id=${encodeURIComponent(queueId)}`
1226
1339
  );
1227
1340
  if (data.matched && data.gameId) {
1228
1341
  output(data);
1229
1342
  return;
1230
1343
  }
1344
+ if (data.readyCheck && data.readyCheckId) {
1345
+ if (!data.alreadyConfirmed) {
1346
+ console.error(
1347
+ `Match found! Game: ${data.gameType ?? "unknown"}, players: ${data.playerCount ?? "?"}. Confirming ready...`
1348
+ );
1349
+ try {
1350
+ const readyResult = await request(
1351
+ "/api/matchmaking/ready",
1352
+ jsonBody({ readyCheckId: data.readyCheckId })
1353
+ );
1354
+ if (readyResult.allReady && readyResult.gameId) {
1355
+ output({ matched: true, gameId: readyResult.gameId, gameType: data.gameType });
1356
+ return;
1357
+ }
1358
+ console.error("Confirmed. Waiting for other players...");
1359
+ } catch {
1360
+ console.error("Ready check failed, returning to queue...");
1361
+ }
1362
+ }
1363
+ continue;
1364
+ }
1365
+ if (data.reason === "expired") {
1366
+ fail("Queue entry expired (stopped polling too long)");
1367
+ }
1231
1368
  } catch {
1232
1369
  await new Promise((r) => setTimeout(r, 2e3));
1233
1370
  }
1234
1371
  }
1372
+ }
1373
+ program.command("quickmatch").description("Queue and wait until matched (system picks the game)").option("--type <type>", "Vote for a game type (optional)").addOption(new Option("--max-players <n>", "(deprecated)").hideHelp()).action(async (opts) => {
1374
+ const body = {};
1375
+ if (opts.type) body.gameType = opts.type;
1376
+ const queueResult = await queueForMatch(body);
1377
+ if (queueResult.matched && queueResult.gameId) {
1378
+ output(queueResult);
1379
+ return;
1380
+ }
1381
+ await pollUntilMatched(queueResult.queueId);
1235
1382
  });
1236
1383
  function askConfirm(question) {
1237
1384
  if (!process.stdin.isTTY) {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@promptedgames/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "packageManager": "pnpm@10.33.0",
4
5
  "description": "CLI for playing games on the Prompted platform. Build AI agents that play poker, Secret Hitler, Coup, Skull, and Liar's Dice.",
5
6
  "type": "module",
6
7
  "license": "MIT",