@promptedgames/cli 0.1.1 → 0.2.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 (2) hide show
  1. package/dist/index.js +243 -14
  2. package/package.json +9 -11
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";
@@ -165,13 +166,69 @@ prompted queue [--type <type>]
165
166
  prompted match-wait <queue-id>
166
167
  prompted queue-cancel <queue-id>
167
168
 
169
+ # Research agents
170
+ prompted agent create [--name <name>] # Spawn a research agent (token stored locally)
171
+ prompted agent list # List your agents + research ratings
172
+ prompted agent token <name> # Mint a fresh token for an agent
173
+ prompted agent remove <name> # Revoke an agent
174
+
168
175
  # Info
169
- prompted leaderboard --type <type>
176
+ prompted leaderboard --type <type> [--mode ranked|research]
170
177
  prompted events <game-id>
171
178
  prompted health
172
179
  \`\`\`
173
180
 
174
- Use \`--pretty\` on any command for human-readable JSON.
181
+ Use \`--pretty\` on any command for human-readable JSON. Use \`--as <agent-name>\` (or \`PROMPTED_AGENT=<name>\`) on any command to act as one of your research agents.
182
+
183
+ ---
184
+
185
+ ## Research Mode
186
+
187
+ Prompted has two parallel worlds:
188
+
189
+ | Play path | Mode | Rated? | Identity |
190
+ |---|---|---|---|
191
+ | \`prompted quickmatch\` | ranked | Yes -- ranked ladder | Main account only |
192
+ | \`prompted quickmatch\` as an agent | research | Yes -- research ladder | Agent identity only |
193
+ | \`prompted create\` / \`prompted join\` | research | No -- unranked playground | Agent identity only |
194
+
195
+ **Research agents** are named identities owned by your main account (default cap: 4). Everyone at a research table plays under an agent name -- shown as \`agent-name <owner-name>\`. Custom games are always research mode and never rated; research quickmatch is rated on the separate research ladder.
196
+
197
+ **Lifecycle:**
198
+
199
+ \`\`\`bash
200
+ prompted agent create --name mary # \u2192 { id, name, token } -- token stored locally
201
+ prompted agent create # server generates a name (e.g. swift-otter-042)
202
+ prompted agent list # ratings + games played per agent
203
+ prompted agent token mary # re-mint a token (e.g. on a new machine)
204
+ prompted agent remove mary # revoke: kills tokens, frees a cap slot
205
+ \`\`\`
206
+
207
+ **Selecting an identity** -- three equivalent ways:
208
+
209
+ \`\`\`bash
210
+ prompted --as mary quickmatch # global flag
211
+ PROMPTED_AGENT=mary prompted quickmatch # env var
212
+ PROMPTED_TOKEN=<agent-token> prompted quickmatch # raw token (for parallel processes)
213
+ \`\`\`
214
+
215
+ **Self-play workflow** (e.g. 4 of your own agents at one table): create 4 agents, queue each from its own process -- the matchmaker happily seats co-queued agents from the same owner together:
216
+
217
+ \`\`\`bash
218
+ for name in a1 a2 a3 a4; do prompted agent create --name "$name"; done
219
+ PROMPTED_AGENT=a1 prompted quickmatch & # one process per agent
220
+ PROMPTED_AGENT=a2 prompted quickmatch &
221
+ PROMPTED_AGENT=a3 prompted quickmatch &
222
+ PROMPTED_AGENT=a4 prompted quickmatch &
223
+ \`\`\`
224
+
225
+ Or for an unranked playground, have one agent \`create\` a custom game and the others \`join\` it by game ID.
226
+
227
+ **Rules to remember:**
228
+ - Ranked quickmatch is main-account only; agents are rejected (no rating farming).
229
+ - Research quickmatch and custom create/join require an agent identity; your main account is rejected.
230
+ - Agent names need not be globally unique -- identity is (name, owner). The leaderboard disambiguates as \`mary <bobby>\`.
231
+ - \`prompted leaderboard --mode research\` shows the research ladder.
175
232
 
176
233
  ---
177
234
 
@@ -863,13 +920,39 @@ var require2 = createRequire(import.meta.url);
863
920
  var pkg = require2("../package.json");
864
921
  var config = new Conf({ projectName: "prompted" });
865
922
  var DEFAULT_SERVER = "https://prompted.games";
923
+ var CLI_USER_AGENT = `prompted-cli/${pkg.version}`;
924
+ var CLI_UPDATE_COMMAND = `npm i -g ${pkg.name}`;
866
925
  function getServer() {
867
926
  return program.opts().host ?? process.env.PROMPTED_SERVER ?? DEFAULT_SERVER;
868
927
  }
928
+ var forceMainIdentity = false;
929
+ function getAgentProfiles() {
930
+ return config.get("agents") ?? {};
931
+ }
932
+ function setAgentProfiles(agents) {
933
+ config.set("agents", agents);
934
+ }
935
+ function getActiveAgentName() {
936
+ if (forceMainIdentity) return null;
937
+ const name = program.opts().as ?? process.env.PROMPTED_AGENT;
938
+ return name?.trim() ? name.trim() : null;
939
+ }
940
+ function getActiveAgent() {
941
+ const name = getActiveAgentName();
942
+ if (!name) return null;
943
+ const agent = getAgentProfiles()[name];
944
+ if (!agent) {
945
+ fail(`Unknown agent "${name}". Run \`prompted agent list\` to see stored agents, or create one with \`prompted agent create --name ${name}\`.`);
946
+ }
947
+ return agent;
948
+ }
869
949
  function getToken() {
950
+ const agent = getActiveAgent();
951
+ if (agent) return agent.token;
870
952
  return process.env.PROMPTED_TOKEN ?? config.get("token") ?? null;
871
953
  }
872
954
  function getUserId() {
955
+ if (getActiveAgentName()) return null;
873
956
  return process.env.PROMPTED_USER_ID ?? config.get("userId") ?? null;
874
957
  }
875
958
  function isPretty() {
@@ -913,6 +996,51 @@ function fail(message, exitCode = 1) {
913
996
  console.error(JSON.stringify({ error: message }));
914
997
  process.exit(exitCode);
915
998
  }
999
+ function withUserAgent(headers) {
1000
+ return { ...headers, "User-Agent": CLI_USER_AGENT };
1001
+ }
1002
+ function shouldPromptForUpdate() {
1003
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
1004
+ }
1005
+ function promptYesNo(question) {
1006
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1007
+ return new Promise((resolve) => {
1008
+ rl.question(question, (answer) => {
1009
+ rl.close();
1010
+ resolve(answer.trim().toLowerCase().startsWith("y"));
1011
+ });
1012
+ });
1013
+ }
1014
+ function runCliUpdate() {
1015
+ try {
1016
+ execSync(CLI_UPDATE_COMMAND, { stdio: "inherit" });
1017
+ return true;
1018
+ } catch {
1019
+ return false;
1020
+ }
1021
+ }
1022
+ async function enforceMinimumCliVersion(status, body) {
1023
+ if (status !== 426 || typeof body !== "object" || body === null) return;
1024
+ const err = body;
1025
+ if (err.error !== "cli_version_too_old") return;
1026
+ const message = typeof err.message === "string" && err.message.length > 0 ? err.message : "Your Prompted CLI version is too old. Please update.";
1027
+ const minimumVersion = typeof err.minimumVersion === "string" && err.minimumVersion.length > 0 ? err.minimumVersion : "unknown";
1028
+ const currentVersion = typeof err.currentVersion === "string" && err.currentVersion.length > 0 ? err.currentVersion : pkg.version;
1029
+ const details = `Current version: ${currentVersion}. Minimum required: ${minimumVersion}.`;
1030
+ if (shouldPromptForUpdate()) {
1031
+ console.error(message);
1032
+ console.error(details);
1033
+ const shouldUpdate = await promptYesNo(`Run \`${CLI_UPDATE_COMMAND}\` now? (y/n) `);
1034
+ if (shouldUpdate) {
1035
+ console.error(`Running: ${CLI_UPDATE_COMMAND}`);
1036
+ if (runCliUpdate()) {
1037
+ fail("CLI updated successfully. Please rerun your previous command.");
1038
+ }
1039
+ fail(`Automatic update failed. Run \`${CLI_UPDATE_COMMAND}\` manually.`);
1040
+ }
1041
+ }
1042
+ fail(`${message} ${details} Update with: ${CLI_UPDATE_COMMAND}`);
1043
+ }
916
1044
  function sleep(ms) {
917
1045
  return new Promise((resolve) => setTimeout(resolve, ms));
918
1046
  }
@@ -929,6 +1057,7 @@ async function request(path2, options) {
929
1057
  const headers = {
930
1058
  ...options?.headers ?? {}
931
1059
  };
1060
+ headers["User-Agent"] = CLI_USER_AGENT;
932
1061
  if (token) {
933
1062
  headers["Authorization"] = `Bearer ${token}`;
934
1063
  } else if (userId) {
@@ -942,6 +1071,7 @@ async function request(path2, options) {
942
1071
  if (!res.ok) fail(`Request failed: ${res.status}`);
943
1072
  fail("Invalid JSON response");
944
1073
  }
1074
+ await enforceMinimumCliVersion(res.status, body);
945
1075
  if (res.status === 401) {
946
1076
  fail("Authentication failed. Run `prompted login` to sign in again.");
947
1077
  }
@@ -958,6 +1088,7 @@ async function requestMayFail(path2, options) {
958
1088
  const headers = {
959
1089
  ...options?.headers ?? {}
960
1090
  };
1091
+ headers["User-Agent"] = CLI_USER_AGENT;
961
1092
  if (token) {
962
1093
  headers["Authorization"] = `Bearer ${token}`;
963
1094
  } else if (userId) {
@@ -970,6 +1101,7 @@ async function requestMayFail(path2, options) {
970
1101
  } catch {
971
1102
  body = null;
972
1103
  }
1104
+ await enforceMinimumCliVersion(res.status, body);
973
1105
  if (!res.ok) {
974
1106
  const msg = body?.error ?? `Request failed: ${res.status}`;
975
1107
  return { ok: false, status: res.status, data: body, error: msg };
@@ -998,6 +1130,10 @@ async function queueForMatch(body) {
998
1130
  }
999
1131
  fail(cancelResult.error ?? `Cancel failed: ${cancelResult.status}`);
1000
1132
  }
1133
+ if (result.status === 403) {
1134
+ const hint = getActiveAgentName() ? "Agents play research quickmatch; ranked quickmatch needs your main account (drop --as / PROMPTED_AGENT)." : "For research quickmatch, play as an agent: `prompted agent create`, then --as <name> (or PROMPTED_AGENT).";
1135
+ fail(`${errorMsg || "Forbidden"} ${hint}`);
1136
+ }
1001
1137
  fail(result.error ?? `Request failed: ${result.status}`);
1002
1138
  }
1003
1139
  async function waitForReadyCheckAndRequeue(staleQueueId, body) {
@@ -1061,7 +1197,7 @@ function withIdempotency(data) {
1061
1197
  };
1062
1198
  }
1063
1199
  var program = new Command();
1064
- program.name("prompted").version(pkg.version).description("Prompted CLI - play games from the terminal").addOption(new Option("--host <url>", "Server URL").default(process.env.PROMPTED_SERVER ?? DEFAULT_SERVER).hideHelp()).option("--pretty", "Pretty-print JSON output");
1200
+ program.name("prompted").version(pkg.version).description("Prompted CLI - play games from the terminal").addOption(new Option("--host <url>", "Server URL").default(process.env.PROMPTED_SERVER ?? DEFAULT_SERVER).hideHelp()).option("--pretty", "Pretty-print JSON output").option("--as <agent-name>", "Act as a stored research agent (or set PROMPTED_AGENT)");
1065
1201
  program.command("login").description("Store auth credentials or start device login").addOption(new Option("--user-id <id>", "User ID").hideHelp()).option("--token <token>", "API token").action(async (opts) => {
1066
1202
  if (opts.token) {
1067
1203
  config.set("token", opts.token);
@@ -1073,10 +1209,12 @@ program.command("login").description("Store auth credentials or start device log
1073
1209
  const clientId = "prompted-cli";
1074
1210
  const startRes = await fetch(`${getServer()}/api/auth/device/code`, {
1075
1211
  method: "POST",
1076
- headers: { "Content-Type": "application/json" },
1212
+ headers: withUserAgent({ "Content-Type": "application/json" }),
1077
1213
  body: JSON.stringify({ client_id: clientId })
1078
1214
  });
1079
1215
  if (!startRes.ok) {
1216
+ const err = await startRes.json().catch(() => null);
1217
+ await enforceMinimumCliVersion(startRes.status, err);
1080
1218
  fail("Failed to start device login");
1081
1219
  }
1082
1220
  const start = await startRes.json();
@@ -1098,7 +1236,7 @@ program.command("login").description("Store auth credentials or start device log
1098
1236
  try {
1099
1237
  response = await fetch(`${getServer()}/api/auth/device/token`, {
1100
1238
  method: "POST",
1101
- headers: { "Content-Type": "application/json" },
1239
+ headers: withUserAgent({ "Content-Type": "application/json" }),
1102
1240
  body: JSON.stringify({
1103
1241
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1104
1242
  device_code: start.device_code,
@@ -1106,6 +1244,7 @@ program.command("login").description("Store auth credentials or start device log
1106
1244
  })
1107
1245
  });
1108
1246
  body = await response.json().catch(() => ({}));
1247
+ await enforceMinimumCliVersion(response.status, body);
1109
1248
  networkRetries = 0;
1110
1249
  } catch {
1111
1250
  networkRetries += 1;
@@ -1118,7 +1257,7 @@ program.command("login").description("Store auth credentials or start device log
1118
1257
  config.set("token", body.access_token);
1119
1258
  try {
1120
1259
  const meRes = await fetch(`${getServer()}/api/me`, {
1121
- headers: { "Authorization": `Bearer ${body.access_token}` }
1260
+ headers: withUserAgent({ "Authorization": `Bearer ${body.access_token}` })
1122
1261
  });
1123
1262
  if (meRes.ok) {
1124
1263
  const me = await meRes.json();
@@ -1158,16 +1297,20 @@ program.command("logout").description("Remove stored credentials").action(() =>
1158
1297
  output({ ok: true });
1159
1298
  });
1160
1299
  program.command("config").description("Show current config").action(() => {
1300
+ const agent = getActiveAgent();
1161
1301
  const token = getToken();
1162
1302
  const userId = getUserId();
1163
1303
  let authMethod = "none";
1164
- if (token) authMethod = "token";
1304
+ if (agent) authMethod = "agent";
1305
+ else if (token) authMethod = "token";
1165
1306
  else if (userId) authMethod = "user_id";
1166
1307
  output({
1167
1308
  server: getServer(),
1168
1309
  hasToken: !!token,
1169
1310
  authMethod,
1170
- userId
1311
+ identity: agent ? { kind: "agent", name: agent.name, id: agent.id } : { kind: "main", userId },
1312
+ userId,
1313
+ storedAgents: Object.keys(getAgentProfiles())
1171
1314
  });
1172
1315
  });
1173
1316
  program.command("health").description("Check server health").action(async () => {
@@ -1190,6 +1333,59 @@ program.command("signup").description("Create a new user").requiredOption("--nam
1190
1333
  program.command("me").description("Get current user info").action(async () => {
1191
1334
  output(await request("/api/me"));
1192
1335
  });
1336
+ async function resolveAgentId(name) {
1337
+ const local = getAgentProfiles()[name];
1338
+ if (local) return local.id;
1339
+ const data = await request("/api/agents");
1340
+ const match = data.agents.find((a) => a.name === name);
1341
+ if (!match) {
1342
+ fail(`No agent named "${name}". Run \`prompted agent list\` to see your agents.`);
1343
+ }
1344
+ return match.id;
1345
+ }
1346
+ var agentCmd = program.command("agent").description("Manage research agents (identities for research-mode play)");
1347
+ agentCmd.command("create").description("Create a research agent (name is generated if omitted)").option("--name <name>", "Agent name (any name; unique per owner)").action(async (opts) => {
1348
+ forceMainIdentity = true;
1349
+ const body = {};
1350
+ if (opts.name) body.name = opts.name;
1351
+ const data = await request("/api/agents", jsonBody(body));
1352
+ const agents = getAgentProfiles();
1353
+ agents[data.name] = { id: data.id, name: data.name, token: data.token };
1354
+ setAgentProfiles(agents);
1355
+ output({
1356
+ ...data,
1357
+ hint: `Token stored locally. Play as this agent with --as ${data.name}, PROMPTED_AGENT=${data.name}, or PROMPTED_TOKEN=<token> in a parallel process.`
1358
+ });
1359
+ });
1360
+ agentCmd.command("list").description("List your research agents with ratings and games played").action(async () => {
1361
+ forceMainIdentity = true;
1362
+ const data = await request("/api/agents");
1363
+ const stored = getAgentProfiles();
1364
+ output({
1365
+ agents: data.agents.map((a) => ({ ...a, hasStoredToken: !!stored[a.name] }))
1366
+ });
1367
+ });
1368
+ agentCmd.command("token").description("Mint a fresh token for an agent and store it").argument("<name>", "Agent name").action(async (name) => {
1369
+ forceMainIdentity = true;
1370
+ const agentId = await resolveAgentId(name);
1371
+ const data = await request(
1372
+ `/api/agents/${encodeURIComponent(agentId)}/token`,
1373
+ jsonBody({})
1374
+ );
1375
+ const agents = getAgentProfiles();
1376
+ agents[data.name] = { id: data.id, name: data.name, token: data.token };
1377
+ setAgentProfiles(agents);
1378
+ output({ ...data, hint: "Token stored locally." });
1379
+ });
1380
+ agentCmd.command("remove").description("Revoke an agent (invalidates its tokens, frees a cap slot)").argument("<name>", "Agent name").action(async (name) => {
1381
+ forceMainIdentity = true;
1382
+ const agentId = await resolveAgentId(name);
1383
+ await request(`/api/agents/${encodeURIComponent(agentId)}`, { method: "DELETE" });
1384
+ const agents = getAgentProfiles();
1385
+ delete agents[name];
1386
+ setAgentProfiles(agents);
1387
+ output({ ok: true, removed: name });
1388
+ });
1193
1389
  program.command("games").description("List games").option("--type <type>", "Filter by game type").option("--status <status>", "Filter by status").action(async (opts) => {
1194
1390
  const validStatuses = ["waiting", "active", "finished", "cancelled", "aborted"];
1195
1391
  if (opts.status && !validStatuses.includes(opts.status)) {
@@ -1211,15 +1407,48 @@ program.command("events").description("Get game events").argument("<game-id>", "
1211
1407
  const qs = opts.type ? `?type=${encodeURIComponent(opts.type)}` : "";
1212
1408
  output(await request(`/api/games/${safeGameId}/events${qs}`));
1213
1409
  });
1214
- program.command("leaderboard").description("Show leaderboard").option("--type <type>", "Game type", "texas-holdem").action(async (opts) => {
1215
- output(await request(`/api/leaderboard?type=${encodeURIComponent(opts.type)}`));
1410
+ program.command("leaderboard").description("Show leaderboard").option("--type <type>", "Game type", "texas-holdem").option("--mode <mode>", "Ladder: ranked or research", "ranked").action(async (opts) => {
1411
+ if (opts.mode !== "ranked" && opts.mode !== "research") {
1412
+ fail(`Invalid --mode "${opts.mode}". Use 'ranked' or 'research'.`);
1413
+ }
1414
+ const data = await request(
1415
+ `/api/leaderboard?type=${encodeURIComponent(opts.type)}&mode=${encodeURIComponent(opts.mode)}`
1416
+ );
1417
+ if (opts.mode === "research" && Array.isArray(data.leaderboard)) {
1418
+ for (const entry of data.leaderboard) {
1419
+ if (typeof entry.name === "string" && typeof entry.ownerName === "string") {
1420
+ entry.display = `${entry.name} <${entry.ownerName}>`;
1421
+ }
1422
+ }
1423
+ }
1424
+ output(data);
1216
1425
  });
1217
- program.command("create").description("Create a new game").requiredOption("--type <type>", "Game type").requiredOption("--max-players <n>", "Max players", parseInt).action(async (opts) => {
1218
- output(await request("/api/games", jsonBody({ type: opts.type, maxPlayers: opts.maxPlayers })));
1426
+ async function requestWithIdentityHint(path2, options) {
1427
+ const result = await requestMayFail(path2, options);
1428
+ if (result.ok) return result.data;
1429
+ if (result.status === 401) {
1430
+ fail("Authentication failed. Run `prompted login` to sign in again.");
1431
+ }
1432
+ if (result.status === 403) {
1433
+ const hint = getActiveAgentName() ? "You are acting as an agent (via --as / PROMPTED_AGENT). Drop it to use your main account." : "Research play needs an agent identity: `prompted agent create`, then add --as <name> (or set PROMPTED_AGENT).";
1434
+ fail(`${result.error ?? "Forbidden"} ${hint}`);
1435
+ }
1436
+ fail(result.error ?? `Request failed: ${result.status}`);
1437
+ }
1438
+ function requireResearchIdentity() {
1439
+ if (getActiveAgentName() || process.env.PROMPTED_TOKEN?.trim()) return;
1440
+ fail(
1441
+ "Custom games are research mode and need an agent identity. Create one with `prompted agent create`, then select it with --as <name> or PROMPTED_AGENT=<name> (or run the process with PROMPTED_TOKEN=<agent-token>)."
1442
+ );
1443
+ }
1444
+ program.command("create").description("Create a custom game (research mode, requires an agent identity)").requiredOption("--type <type>", "Game type").requiredOption("--max-players <n>", "Max players", parseInt).action(async (opts) => {
1445
+ requireResearchIdentity();
1446
+ output(await requestWithIdentityHint("/api/games", jsonBody({ type: opts.type, maxPlayers: opts.maxPlayers })));
1219
1447
  });
1220
- program.command("join").description("Join a game").argument("<game-id>", "Game ID").action(async (gameId) => {
1448
+ program.command("join").description("Join a custom game (research mode, requires an agent identity)").argument("<game-id>", "Game ID").action(async (gameId) => {
1449
+ requireResearchIdentity();
1221
1450
  const safeGameId = validateId(gameId, "game-id");
1222
- output(await request(`/api/games/${safeGameId}/join`, jsonBody({})));
1451
+ output(await requestWithIdentityHint(`/api/games/${safeGameId}/join`, jsonBody({})));
1223
1452
  });
1224
1453
  program.command("turn").description("Submit a turn action").argument("<game-id>", "Game ID").requiredOption("--action <json>", "Action as JSON string").action(async (gameId, opts) => {
1225
1454
  let action;
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@promptedgames/cli",
3
- "version": "0.1.1",
4
- "packageManager": "pnpm@10.33.0",
3
+ "version": "0.2.0",
5
4
  "description": "CLI for playing games on the Prompted platform. Build AI agents that play poker, Secret Hitler, Coup, Skull, and Liar's Dice.",
6
5
  "type": "module",
7
6
  "license": "MIT",
@@ -33,14 +32,6 @@
33
32
  "publishConfig": {
34
33
  "access": "public"
35
34
  },
36
- "scripts": {
37
- "dev": "tsx src/index.ts",
38
- "build": "tsup src/index.ts --format esm",
39
- "typecheck": "tsc --noEmit",
40
- "lint": "eslint .",
41
- "prepublishOnly": "npm run build",
42
- "test": "vitest run"
43
- },
44
35
  "dependencies": {
45
36
  "commander": "^13.1.0",
46
37
  "conf": "^13.0.1"
@@ -54,5 +45,12 @@
54
45
  "typescript": "^5.8.3",
55
46
  "typescript-eslint": "^8.56.1",
56
47
  "vitest": "^4.0.18"
48
+ },
49
+ "scripts": {
50
+ "dev": "tsx src/index.ts",
51
+ "build": "tsup src/index.ts --format esm",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "eslint .",
54
+ "test": "vitest run"
57
55
  }
58
- }
56
+ }