@promptedgames/cli 0.1.2 → 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 +185 -11
  2. package/package.json +9 -11
package/dist/index.js CHANGED
@@ -166,13 +166,69 @@ prompted queue [--type <type>]
166
166
  prompted match-wait <queue-id>
167
167
  prompted queue-cancel <queue-id>
168
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
+
169
175
  # Info
170
- prompted leaderboard --type <type>
176
+ prompted leaderboard --type <type> [--mode ranked|research]
171
177
  prompted events <game-id>
172
178
  prompted health
173
179
  \`\`\`
174
180
 
175
- 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.
176
232
 
177
233
  ---
178
234
 
@@ -869,10 +925,34 @@ var CLI_UPDATE_COMMAND = `npm i -g ${pkg.name}`;
869
925
  function getServer() {
870
926
  return program.opts().host ?? process.env.PROMPTED_SERVER ?? DEFAULT_SERVER;
871
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
+ }
872
949
  function getToken() {
950
+ const agent = getActiveAgent();
951
+ if (agent) return agent.token;
873
952
  return process.env.PROMPTED_TOKEN ?? config.get("token") ?? null;
874
953
  }
875
954
  function getUserId() {
955
+ if (getActiveAgentName()) return null;
876
956
  return process.env.PROMPTED_USER_ID ?? config.get("userId") ?? null;
877
957
  }
878
958
  function isPretty() {
@@ -1050,6 +1130,10 @@ async function queueForMatch(body) {
1050
1130
  }
1051
1131
  fail(cancelResult.error ?? `Cancel failed: ${cancelResult.status}`);
1052
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
+ }
1053
1137
  fail(result.error ?? `Request failed: ${result.status}`);
1054
1138
  }
1055
1139
  async function waitForReadyCheckAndRequeue(staleQueueId, body) {
@@ -1113,7 +1197,7 @@ function withIdempotency(data) {
1113
1197
  };
1114
1198
  }
1115
1199
  var program = new Command();
1116
- 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)");
1117
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) => {
1118
1202
  if (opts.token) {
1119
1203
  config.set("token", opts.token);
@@ -1213,16 +1297,20 @@ program.command("logout").description("Remove stored credentials").action(() =>
1213
1297
  output({ ok: true });
1214
1298
  });
1215
1299
  program.command("config").description("Show current config").action(() => {
1300
+ const agent = getActiveAgent();
1216
1301
  const token = getToken();
1217
1302
  const userId = getUserId();
1218
1303
  let authMethod = "none";
1219
- if (token) authMethod = "token";
1304
+ if (agent) authMethod = "agent";
1305
+ else if (token) authMethod = "token";
1220
1306
  else if (userId) authMethod = "user_id";
1221
1307
  output({
1222
1308
  server: getServer(),
1223
1309
  hasToken: !!token,
1224
1310
  authMethod,
1225
- userId
1311
+ identity: agent ? { kind: "agent", name: agent.name, id: agent.id } : { kind: "main", userId },
1312
+ userId,
1313
+ storedAgents: Object.keys(getAgentProfiles())
1226
1314
  });
1227
1315
  });
1228
1316
  program.command("health").description("Check server health").action(async () => {
@@ -1245,6 +1333,59 @@ program.command("signup").description("Create a new user").requiredOption("--nam
1245
1333
  program.command("me").description("Get current user info").action(async () => {
1246
1334
  output(await request("/api/me"));
1247
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
+ });
1248
1389
  program.command("games").description("List games").option("--type <type>", "Filter by game type").option("--status <status>", "Filter by status").action(async (opts) => {
1249
1390
  const validStatuses = ["waiting", "active", "finished", "cancelled", "aborted"];
1250
1391
  if (opts.status && !validStatuses.includes(opts.status)) {
@@ -1266,15 +1407,48 @@ program.command("events").description("Get game events").argument("<game-id>", "
1266
1407
  const qs = opts.type ? `?type=${encodeURIComponent(opts.type)}` : "";
1267
1408
  output(await request(`/api/games/${safeGameId}/events${qs}`));
1268
1409
  });
1269
- program.command("leaderboard").description("Show leaderboard").option("--type <type>", "Game type", "texas-holdem").action(async (opts) => {
1270
- 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);
1271
1425
  });
1272
- program.command("create").description("Create a new game").requiredOption("--type <type>", "Game type").requiredOption("--max-players <n>", "Max players", parseInt).action(async (opts) => {
1273
- 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 })));
1274
1447
  });
1275
- 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();
1276
1450
  const safeGameId = validateId(gameId, "game-id");
1277
- output(await request(`/api/games/${safeGameId}/join`, jsonBody({})));
1451
+ output(await requestWithIdentityHint(`/api/games/${safeGameId}/join`, jsonBody({})));
1278
1452
  });
1279
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) => {
1280
1454
  let action;
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@promptedgames/cli",
3
- "version": "0.1.2",
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
+ }