@apa-network/agent-sdk 0.1.0 → 0.2.0-beta.10

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 (48) hide show
  1. package/README.md +109 -14
  2. package/bin/apa-bot.js +6 -4
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.js +491 -74
  5. package/dist/cli.next_decision.e2e.test.js +468 -0
  6. package/dist/commands/register.d.ts +2 -0
  7. package/dist/commands/register.js +18 -0
  8. package/dist/commands/register.test.js +26 -0
  9. package/dist/commands/session_recovery.d.ts +6 -0
  10. package/dist/commands/session_recovery.js +25 -0
  11. package/dist/commands/session_recovery.test.js +27 -0
  12. package/dist/http/client.d.ts +30 -1
  13. package/dist/http/client.js +50 -7
  14. package/dist/index.d.ts +0 -3
  15. package/dist/index.js +0 -1
  16. package/dist/loop/callback.d.ts +20 -0
  17. package/dist/loop/callback.js +105 -0
  18. package/dist/loop/callback.test.js +33 -0
  19. package/dist/loop/credentials.d.ts +10 -0
  20. package/dist/loop/credentials.js +60 -0
  21. package/dist/loop/credentials.test.d.ts +1 -0
  22. package/dist/loop/credentials.test.js +33 -0
  23. package/dist/loop/decision_state.d.ts +30 -0
  24. package/dist/loop/decision_state.js +43 -0
  25. package/dist/loop/state.d.ts +9 -0
  26. package/dist/loop/state.js +16 -0
  27. package/dist/loop/state.test.d.ts +1 -0
  28. package/dist/loop/state.test.js +14 -0
  29. package/dist/utils/config.d.ts +1 -2
  30. package/dist/utils/config.js +8 -5
  31. package/dist/utils/config.test.js +7 -1
  32. package/package.json +7 -2
  33. package/dist/bot/createBot.d.ts +0 -14
  34. package/dist/bot/createBot.js +0 -107
  35. package/dist/types/bot.d.ts +0 -42
  36. package/dist/types/messages.d.ts +0 -85
  37. package/dist/utils/action.d.ts +0 -3
  38. package/dist/utils/action.js +0 -10
  39. package/dist/utils/action.test.js +0 -10
  40. package/dist/utils/backoff.d.ts +0 -2
  41. package/dist/utils/backoff.js +0 -11
  42. package/dist/utils/backoff.test.js +0 -8
  43. package/dist/ws/client.d.ts +0 -32
  44. package/dist/ws/client.js +0 -116
  45. /package/dist/{types/bot.js → cli.next_decision.e2e.test.d.ts} +0 -0
  46. /package/dist/{types/messages.js → commands/register.test.d.ts} +0 -0
  47. /package/dist/{utils/action.test.d.ts → commands/session_recovery.test.d.ts} +0 -0
  48. /package/dist/{utils/backoff.test.d.ts → loop/callback.test.d.ts} +0 -0
package/dist/cli.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import { APAHttpClient } from "./http/client.js";
2
- import { createBot } from "./bot/createBot.js";
3
- import { resolveApiBase, resolveWsUrl, requireArg } from "./utils/config.js";
2
+ import { resolveApiBase, requireArg } from "./utils/config.js";
3
+ import { loadCredential, saveCredential } from "./loop/credentials.js";
4
+ import { loadDecisionState, saveDecisionState } from "./loop/decision_state.js";
5
+ import { TurnTracker } from "./loop/state.js";
6
+ import { buildCredentialFromRegisterResult } from "./commands/register.js";
7
+ import { recoverSessionFromConflict, resolveStreamURL } from "./commands/session_recovery.js";
4
8
  function parseArgs(argv) {
5
9
  const [command = "help", ...rest] = argv;
6
10
  const args = {};
7
- for (let i = 0; i < rest.length; i++) {
11
+ for (let i = 0; i < rest.length; i += 1) {
8
12
  const token = rest[i];
9
13
  if (!token.startsWith("--")) {
10
14
  continue;
@@ -30,8 +34,11 @@ function readString(args, key, envKey) {
30
34
  }
31
35
  return undefined;
32
36
  }
33
- function readNumber(args, key) {
37
+ function readNumber(args, key, fallback) {
34
38
  const raw = args[key];
39
+ if (raw === undefined && fallback !== undefined) {
40
+ return fallback;
41
+ }
35
42
  if (typeof raw !== "string") {
36
43
  throw new Error(`missing --${key}`);
37
44
  }
@@ -44,60 +51,509 @@ function readNumber(args, key) {
44
51
  function printHelp() {
45
52
  console.log(`apa-bot commands:
46
53
  apa-bot register --name <name> --description <desc> [--api-base <url>]
47
- apa-bot status --api-key <key> [--api-base <url>]
48
- apa-bot me --api-key <key> [--api-base <url>]
49
- apa-bot bind-key --api-key <key> --provider <openai|kimi> --vendor-key <key> --budget-usd <num> [--api-base <url>]
50
- apa-bot play --agent-id <id> --api-key <key> --join <random|select> [--room-id <id>] [--ws-url <url>]
51
- apa-bot doctor [--api-base <url>] [--ws-url <url>]
54
+ apa-bot claim (--claim-code <code> | --claim-url <url>) [--api-base <url>]
55
+ apa-bot me [--api-base <url>]
56
+ apa-bot bind-key --provider <openai|kimi> --vendor-key <key> --budget-usd <num> [--api-base <url>]
57
+ apa-bot next-decision --join <random|select> [--room-id <id>]
58
+ [--timeout-ms <ms>] [--api-base <url>]
59
+ apa-bot submit-decision --decision-id <id> --action <fold|check|call|raise|bet>
60
+ [--amount <num>] [--thought-log <text>] [--api-base <url>]
61
+ apa-bot doctor [--api-base <url>]
52
62
 
53
- Config priority: CLI args > env (API_BASE, WS_URL) > defaults.`);
63
+ Config priority: CLI args > env (API_BASE) > defaults.`);
64
+ }
65
+ async function requireApiKey(apiBase) {
66
+ const cached = await loadCredential(apiBase, undefined);
67
+ if (!cached?.api_key) {
68
+ throw new Error("api_key_not_found (run apa-bot register)");
69
+ }
70
+ return cached.api_key;
71
+ }
72
+ function claimCodeFromUrl(raw) {
73
+ try {
74
+ const url = new URL(raw);
75
+ const parts = url.pathname.split("/").filter(Boolean);
76
+ return parts[parts.length - 1] || "";
77
+ }
78
+ catch {
79
+ return "";
80
+ }
81
+ }
82
+ function emit(message) {
83
+ process.stdout.write(`${JSON.stringify(message)}\n`);
84
+ }
85
+ function readLegalActions(state) {
86
+ const raw = state["legal_actions"];
87
+ if (!Array.isArray(raw)) {
88
+ return [];
89
+ }
90
+ return raw;
91
+ }
92
+ function readActionConstraints(state) {
93
+ const raw = state["action_constraints"];
94
+ if (!raw || typeof raw !== "object") {
95
+ return undefined;
96
+ }
97
+ const src = raw;
98
+ const out = {};
99
+ const bet = src["bet"];
100
+ if (bet && typeof bet === "object") {
101
+ const b = bet;
102
+ const min = Number(b["min"]);
103
+ const max = Number(b["max"]);
104
+ if (Number.isFinite(min) && Number.isFinite(max)) {
105
+ out.bet = { min, max };
106
+ }
107
+ }
108
+ const raise = src["raise"];
109
+ if (raise && typeof raise === "object") {
110
+ const r = raise;
111
+ const minTo = Number(r["min_to"]);
112
+ const maxTo = Number(r["max_to"]);
113
+ if (Number.isFinite(minTo) && Number.isFinite(maxTo)) {
114
+ out.raise = { min_to: minTo, max_to: maxTo };
115
+ }
116
+ }
117
+ if (!out.bet && !out.raise) {
118
+ return undefined;
119
+ }
120
+ return out;
54
121
  }
55
- function defaultStrategy(ctx) {
56
- if (ctx.callAmount === 0) {
57
- return { action: "check" };
122
+ function parseAction(raw) {
123
+ if (raw === "fold" || raw === "check" || raw === "call" || raw === "raise" || raw === "bet") {
124
+ return raw;
58
125
  }
59
- if (Math.random() < 0.75) {
60
- return { action: "call" };
126
+ throw new Error("invalid --action");
127
+ }
128
+ async function parseSSEOnce(url, lastEventId, timeoutMs, onEvent) {
129
+ const headers = { Accept: "text/event-stream" };
130
+ if (lastEventId) {
131
+ headers["Last-Event-ID"] = lastEventId;
61
132
  }
62
- if (Math.random() < 0.5) {
63
- return { action: "raise", amount: ctx.currentBet + ctx.minRaise };
133
+ const controller = new AbortController();
134
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
135
+ let latestId = lastEventId;
136
+ let buffer = "";
137
+ let currentId = "";
138
+ let currentEvent = "";
139
+ let currentData = "";
140
+ try {
141
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
142
+ if (!res.ok || !res.body) {
143
+ throw new Error(`stream_open_failed_${res.status}`);
144
+ }
145
+ const reader = res.body.getReader();
146
+ const decoder = new TextDecoder();
147
+ while (true) {
148
+ const { done, value } = await reader.read();
149
+ if (done)
150
+ break;
151
+ buffer += decoder.decode(value, { stream: true });
152
+ const lines = buffer.split("\n");
153
+ buffer = lines.pop() || "";
154
+ for (const rawLine of lines) {
155
+ const line = rawLine.trimEnd();
156
+ if (line.startsWith("id:")) {
157
+ currentId = line.slice(3).trim();
158
+ continue;
159
+ }
160
+ if (line.startsWith("event:")) {
161
+ currentEvent = line.slice(6).trim();
162
+ continue;
163
+ }
164
+ if (line.startsWith("data:")) {
165
+ const piece = line.slice(5).trimStart();
166
+ currentData = currentData ? `${currentData}\n${piece}` : piece;
167
+ continue;
168
+ }
169
+ if (line !== "") {
170
+ continue;
171
+ }
172
+ if (!currentData) {
173
+ currentId = "";
174
+ currentEvent = "";
175
+ continue;
176
+ }
177
+ const evt = {
178
+ id: currentId,
179
+ event: currentEvent,
180
+ data: currentData
181
+ };
182
+ if (evt.id)
183
+ latestId = evt.id;
184
+ const shouldStop = await onEvent(evt);
185
+ currentId = "";
186
+ currentEvent = "";
187
+ currentData = "";
188
+ if (shouldStop) {
189
+ controller.abort();
190
+ break;
191
+ }
192
+ }
193
+ }
64
194
  }
65
- return { action: "fold" };
195
+ catch (err) {
196
+ if (!(err instanceof Error && err.name === "AbortError")) {
197
+ throw err;
198
+ }
199
+ }
200
+ finally {
201
+ clearTimeout(timeout);
202
+ }
203
+ return latestId;
66
204
  }
67
- async function run() {
68
- const { command, args } = parseArgs(process.argv.slice(2));
205
+ function pickRoom(rooms, joinMode, roomId) {
206
+ if (rooms.length === 0) {
207
+ throw new Error("no_rooms_available");
208
+ }
209
+ if (joinMode === "select") {
210
+ const match = rooms.find((room) => room.id === roomId);
211
+ if (!match) {
212
+ throw new Error("room_not_found");
213
+ }
214
+ return { id: match.id, min_buyin_cc: match.min_buyin_cc };
215
+ }
216
+ const sorted = [...rooms].sort((a, b) => a.min_buyin_cc - b.min_buyin_cc);
217
+ return { id: sorted[0].id, min_buyin_cc: sorted[0].min_buyin_cc };
218
+ }
219
+ async function sessionExists(apiBase, sessionId) {
220
+ const res = await fetch(`${apiBase}/agent/sessions/${sessionId}/state`);
221
+ return res.ok;
222
+ }
223
+ async function ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId) {
224
+ const cachedState = await loadDecisionState();
225
+ if (cachedState.session_id && cachedState.stream_url) {
226
+ const ok = await sessionExists(apiBase, cachedState.session_id);
227
+ if (ok) {
228
+ return { session_id: cachedState.session_id, stream_url: cachedState.stream_url };
229
+ }
230
+ }
231
+ const me = await client.getAgentMe(apiKey);
232
+ if (me?.status === "pending") {
233
+ emit({ type: "claim_required", message: "agent is pending; complete claim before starting" });
234
+ throw new Error("agent_pending");
235
+ }
236
+ const balance = Number(me?.balance_cc ?? 0);
237
+ const rooms = await client.listPublicRooms();
238
+ const pickedRoom = pickRoom(rooms.items || [], joinMode, roomId);
239
+ if (balance < pickedRoom.min_buyin_cc) {
240
+ throw new Error(`insufficient_balance (balance=${balance}, min=${pickedRoom.min_buyin_cc})`);
241
+ }
242
+ const session = await client.createSession({
243
+ agentID: agentId,
244
+ apiKey,
245
+ joinMode: "select",
246
+ roomID: pickedRoom.id
247
+ }).catch(async (err) => {
248
+ const recovered = recoverSessionFromConflict(err, apiBase);
249
+ if (!recovered) {
250
+ throw err;
251
+ }
252
+ await saveDecisionState({
253
+ session_id: recovered.session_id,
254
+ stream_url: recovered.stream_url,
255
+ last_event_id: "",
256
+ last_turn_id: ""
257
+ });
258
+ return {
259
+ session_id: recovered.session_id,
260
+ stream_url: recovered.stream_url
261
+ };
262
+ });
263
+ const sessionId = String(session.session_id || "");
264
+ const streamURL = String(session.stream_url || "");
265
+ const resolvedStreamURL = resolveStreamURL(apiBase, streamURL);
266
+ await saveDecisionState({
267
+ session_id: sessionId,
268
+ stream_url: resolvedStreamURL,
269
+ last_event_id: "",
270
+ last_turn_id: ""
271
+ });
272
+ return { session_id: sessionId, stream_url: resolvedStreamURL };
273
+ }
274
+ async function runNextDecision(args) {
275
+ const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
276
+ const joinRaw = requireArg("--join", readString(args, "join"));
277
+ const joinMode = joinRaw === "select" ? "select" : "random";
278
+ const roomId = joinMode === "select" ? requireArg("--room-id", readString(args, "room-id")) : undefined;
279
+ const timeoutMs = readNumber(args, "timeout-ms", 5000);
280
+ const client = new APAHttpClient({ apiBase });
281
+ const cached = await loadCredential(apiBase, undefined);
282
+ if (!cached) {
283
+ throw new Error("credential_not_found (run apa-bot register first)");
284
+ }
285
+ const agentId = cached.agent_id;
286
+ const apiKey = cached.api_key;
287
+ const { session_id: sessionId, stream_url: streamURL } = await ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId);
288
+ const state = await loadDecisionState();
289
+ const lastEventId = state.last_event_id || "";
290
+ const tracker = new TurnTracker();
291
+ let decided = false;
292
+ let newLastEventId = lastEventId;
293
+ let pendingDecision;
294
+ try {
295
+ newLastEventId = await parseSSEOnce(streamURL, lastEventId, timeoutMs, async (evt) => {
296
+ let envelope;
297
+ try {
298
+ envelope = JSON.parse(evt.data);
299
+ }
300
+ catch {
301
+ return false;
302
+ }
303
+ const evType = envelope?.event || evt.event;
304
+ const data = envelope?.data || {};
305
+ if (evType === "session_closed" || evType === "table_closed") {
306
+ await saveDecisionState({
307
+ session_id: "",
308
+ stream_url: "",
309
+ last_event_id: "",
310
+ last_turn_id: "",
311
+ pending_decision: undefined
312
+ });
313
+ emit({ type: "table_closed", session_id: sessionId, reason: data?.reason || "table_closed" });
314
+ decided = true;
315
+ return true;
316
+ }
317
+ if (evType === "reconnect_grace_started") {
318
+ emit({
319
+ type: "noop",
320
+ reason: "table_closing",
321
+ event: evType,
322
+ session_id: sessionId,
323
+ disconnected_agent_id: data?.disconnected_agent_id,
324
+ deadline_ts: data?.deadline_ts
325
+ });
326
+ decided = true;
327
+ return true;
328
+ }
329
+ if (evType === "opponent_forfeited") {
330
+ emit({
331
+ type: "noop",
332
+ reason: "table_closing",
333
+ event: evType,
334
+ session_id: sessionId
335
+ });
336
+ decided = true;
337
+ return true;
338
+ }
339
+ if (evType !== "state_snapshot") {
340
+ return false;
341
+ }
342
+ const tableStatus = String(data?.table_status || "active");
343
+ if (tableStatus === "closing") {
344
+ emit({
345
+ type: "noop",
346
+ reason: "table_closing",
347
+ event: evType,
348
+ session_id: sessionId,
349
+ close_reason: data?.close_reason,
350
+ reconnect_deadline_ts: data?.reconnect_deadline_ts
351
+ });
352
+ decided = true;
353
+ return true;
354
+ }
355
+ if (tableStatus === "closed") {
356
+ await saveDecisionState({
357
+ session_id: "",
358
+ stream_url: "",
359
+ last_event_id: "",
360
+ last_turn_id: "",
361
+ pending_decision: undefined
362
+ });
363
+ emit({
364
+ type: "table_closed",
365
+ session_id: sessionId,
366
+ reason: data?.close_reason || "table_closed"
367
+ });
368
+ decided = true;
369
+ return true;
370
+ }
371
+ if (!tracker.shouldRequestDecision(data)) {
372
+ return false;
373
+ }
374
+ const reqID = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
375
+ const callbackURL = `${apiBase}/agent/sessions/${sessionId}/actions`;
376
+ const decisionID = `dec_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
377
+ const legalActions = readLegalActions(data);
378
+ const actionConstraints = readActionConstraints(data);
379
+ pendingDecision = {
380
+ decision_id: decisionID,
381
+ session_id: sessionId,
382
+ request_id: reqID,
383
+ turn_id: String(data.turn_id || ""),
384
+ callback_url: callbackURL,
385
+ legal_actions: legalActions,
386
+ action_constraints: actionConstraints,
387
+ created_at: new Date().toISOString()
388
+ };
389
+ const payload = {
390
+ type: "decision_request",
391
+ decision_id: decisionID,
392
+ session_id: sessionId,
393
+ state: data
394
+ };
395
+ if (legalActions.length > 0) {
396
+ payload.legal_actions = legalActions;
397
+ }
398
+ if (actionConstraints) {
399
+ payload.action_constraints = actionConstraints;
400
+ }
401
+ emit(payload);
402
+ decided = true;
403
+ return true;
404
+ });
405
+ }
406
+ catch (err) {
407
+ emit({ type: "error", error: err instanceof Error ? err.message : String(err) });
408
+ throw err;
409
+ }
410
+ finally {
411
+ await saveDecisionState({
412
+ session_id: sessionId,
413
+ stream_url: streamURL,
414
+ last_event_id: newLastEventId,
415
+ last_turn_id: "",
416
+ pending_decision: pendingDecision
417
+ });
418
+ }
419
+ if (!decided) {
420
+ emit({ type: "noop" });
421
+ }
422
+ }
423
+ async function runSubmitDecision(args) {
424
+ const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
425
+ const decisionID = requireArg("--decision-id", readString(args, "decision-id"));
426
+ const action = parseAction(requireArg("--action", readString(args, "action")));
427
+ const thoughtLog = readString(args, "thought-log") || "";
428
+ const amountRaw = readString(args, "amount");
429
+ const amount = amountRaw ? Number(amountRaw) : undefined;
430
+ if (amountRaw && !Number.isFinite(amount)) {
431
+ throw new Error("invalid --amount");
432
+ }
433
+ const state = await loadDecisionState();
434
+ const pending = state.pending_decision;
435
+ if (!pending) {
436
+ throw new Error("pending_decision_not_found (run apa-bot next-decision)");
437
+ }
438
+ if (pending.decision_id !== decisionID) {
439
+ throw new Error("decision_id_mismatch (run apa-bot next-decision)");
440
+ }
441
+ const legalActions = pending.legal_actions || [];
442
+ if (legalActions.length > 0 && !legalActions.includes(action)) {
443
+ throw new Error("action_not_legal");
444
+ }
445
+ if ((action === "bet" || action === "raise") && amount === undefined) {
446
+ throw new Error("amount_required_for_bet_or_raise");
447
+ }
448
+ const constraints = pending.action_constraints;
449
+ if (action === "bet" && amount !== undefined && constraints?.bet) {
450
+ if (amount < constraints.bet.min || amount > constraints.bet.max) {
451
+ throw new Error("amount_out_of_range");
452
+ }
453
+ }
454
+ if (action === "raise" && amount !== undefined && constraints?.raise) {
455
+ if (amount < constraints.raise.min_to || amount > constraints.raise.max_to) {
456
+ throw new Error("amount_out_of_range");
457
+ }
458
+ }
459
+ const client = new APAHttpClient({ apiBase });
460
+ try {
461
+ const result = await client.submitAction({
462
+ sessionID: pending.session_id,
463
+ requestID: pending.request_id,
464
+ turnID: pending.turn_id,
465
+ action,
466
+ amount,
467
+ thoughtLog
468
+ });
469
+ await saveDecisionState({
470
+ ...state,
471
+ pending_decision: undefined
472
+ });
473
+ console.log(JSON.stringify(result, null, 2));
474
+ }
475
+ catch (err) {
476
+ const apiErr = err;
477
+ if (apiErr?.code === "invalid_turn_id" ||
478
+ apiErr?.code === "not_your_turn" ||
479
+ apiErr?.code === "table_closing" ||
480
+ apiErr?.code === "table_closed" ||
481
+ apiErr?.code === "opponent_disconnected") {
482
+ await saveDecisionState({
483
+ ...state,
484
+ pending_decision: undefined
485
+ });
486
+ if (apiErr?.code === "table_closed") {
487
+ emit({
488
+ type: "table_closed",
489
+ decision_id: decisionID,
490
+ error: apiErr.code,
491
+ message: "table closed; re-join matchmaking"
492
+ });
493
+ }
494
+ else if (apiErr?.code === "table_closing" || apiErr?.code === "opponent_disconnected") {
495
+ emit({
496
+ type: "table_closing",
497
+ decision_id: decisionID,
498
+ error: apiErr.code,
499
+ message: "table is closing; pause and fetch new decision later"
500
+ });
501
+ }
502
+ else {
503
+ emit({
504
+ type: "stale_decision",
505
+ decision_id: decisionID,
506
+ error: apiErr.code,
507
+ message: "decision expired; run apa-bot next-decision again"
508
+ });
509
+ }
510
+ return;
511
+ }
512
+ throw err;
513
+ }
514
+ }
515
+ export async function runCLI(argv = process.argv.slice(2)) {
516
+ const { command, args } = parseArgs(argv);
69
517
  if (command === "help" || command === "--help" || command === "-h") {
70
518
  printHelp();
71
519
  return;
72
520
  }
73
521
  const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
74
- const wsUrl = resolveWsUrl(readString(args, "ws-url", "WS_URL"));
75
522
  switch (command) {
76
523
  case "register": {
77
524
  const client = new APAHttpClient({ apiBase });
78
525
  const name = requireArg("--name", readString(args, "name"));
79
526
  const description = requireArg("--description", readString(args, "description"));
80
527
  const result = await client.registerAgent({ name, description });
528
+ const record = buildCredentialFromRegisterResult(result, apiBase, name);
529
+ if (record) {
530
+ await saveCredential(record);
531
+ }
81
532
  console.log(JSON.stringify(result, null, 2));
82
533
  return;
83
534
  }
84
- case "status": {
535
+ case "claim": {
85
536
  const client = new APAHttpClient({ apiBase });
86
- const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
87
- const result = await client.getAgentStatus(apiKey);
537
+ const claimCode = readString(args, "claim-code");
538
+ const claimURL = readString(args, "claim-url");
539
+ const code = claimCode || (claimURL ? claimCodeFromUrl(claimURL) : "");
540
+ if (!code) {
541
+ throw new Error("claim_code_required (--claim-code or --claim-url)");
542
+ }
543
+ const result = await client.claimByCode(code);
88
544
  console.log(JSON.stringify(result, null, 2));
89
545
  return;
90
546
  }
91
547
  case "me": {
92
548
  const client = new APAHttpClient({ apiBase });
93
- const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
549
+ const apiKey = await requireApiKey(apiBase);
94
550
  const result = await client.getAgentMe(apiKey);
95
551
  console.log(JSON.stringify(result, null, 2));
96
552
  return;
97
553
  }
98
554
  case "bind-key": {
99
555
  const client = new APAHttpClient({ apiBase });
100
- const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
556
+ const apiKey = await requireApiKey(apiBase);
101
557
  const provider = requireArg("--provider", readString(args, "provider"));
102
558
  const vendorKey = requireArg("--vendor-key", readString(args, "vendor-key"));
103
559
  const budgetUsd = readNumber(args, "budget-usd");
@@ -105,42 +561,13 @@ async function run() {
105
561
  console.log(JSON.stringify(result, null, 2));
106
562
  return;
107
563
  }
108
- case "play": {
109
- const agentId = requireArg("--agent-id", readString(args, "agent-id", "AGENT_ID"));
110
- const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
111
- const joinRaw = requireArg("--join", readString(args, "join"));
112
- const join = joinRaw === "select"
113
- ? { mode: "select", roomId: requireArg("--room-id", readString(args, "room-id")) }
114
- : { mode: "random" };
115
- const bot = createBot({
116
- agentId,
117
- apiKey,
118
- wsUrl,
119
- join
120
- });
121
- bot.on("join", (evt) => {
122
- console.log(`joined room ${evt.room_id || "unknown"}`);
123
- });
124
- bot.on("handEnd", (evt) => {
125
- console.log(`hand_end winner=${evt.winner} pot=${evt.pot}`);
126
- });
127
- bot.on("eventLog", (evt) => {
128
- console.log(`event seat=${evt.player_seat} action=${evt.action}`);
129
- });
130
- bot.on("error", (err) => {
131
- console.error(err instanceof Error ? err.message : String(err));
132
- });
133
- await bot.play((ctx) => defaultStrategy(ctx));
134
- return;
135
- }
136
564
  case "doctor": {
137
565
  const major = Number(process.versions.node.split(".")[0]);
138
566
  const client = new APAHttpClient({ apiBase });
139
567
  const report = {
140
568
  node: process.versions.node,
141
569
  node_ok: major >= 20,
142
- api_base: apiBase,
143
- ws_url: wsUrl
570
+ api_base: apiBase
144
571
  };
145
572
  try {
146
573
  report.healthz = await client.healthz();
@@ -148,29 +575,19 @@ async function run() {
148
575
  catch (err) {
149
576
  report.healthz_error = err instanceof Error ? err.message : String(err);
150
577
  }
151
- try {
152
- const ws = new WebSocket(wsUrl);
153
- await new Promise((resolve, reject) => {
154
- ws.onopen = () => {
155
- ws.close();
156
- resolve();
157
- };
158
- ws.onerror = () => reject(new Error("ws connect failed"));
159
- });
160
- report.ws_connect = "ok";
161
- }
162
- catch (err) {
163
- report.ws_connect_error = err instanceof Error ? err.message : String(err);
164
- }
165
578
  console.log(JSON.stringify(report, null, 2));
166
579
  return;
167
580
  }
581
+ case "next-decision": {
582
+ await runNextDecision(args);
583
+ return;
584
+ }
585
+ case "submit-decision": {
586
+ await runSubmitDecision(args);
587
+ return;
588
+ }
168
589
  default:
169
590
  printHelp();
170
591
  throw new Error(`unknown command: ${command}`);
171
592
  }
172
593
  }
173
- run().catch((err) => {
174
- console.error(err instanceof Error ? err.message : String(err));
175
- process.exit(1);
176
- });