@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.
- package/dist/index.js +165 -18
- 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
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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",
|