@apa-network/agent-sdk 0.2.0-beta.1 → 0.2.0-beta.12

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