pyyol 1.14.0 → 1.14.1

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
@@ -19,7 +19,7 @@ import { deriveConnectUrl, deviceLabel, runLoginFlow } from "./login.js";
19
19
  import * as mode from "./mode.js";
20
20
  import { RuntimeConnector } from "./runtime.js";
21
21
  import { askWatch, watchUrl, WATCH_BROWSER, WATCH_TERMINAL } from "./watch.js";
22
- import { INTENT_INVITE, friendsUrl, resolveStartupIntent, } from "./intent.js";
22
+ import { INTENT_INVITE, friendsUrl, isInsufficientBalance, offerBuyCoins, resolveStartupIntent, } from "./intent.js";
23
23
  import { REQUEST_ID_HEADER, SIGNATURE_HEADER, SIGNATURE_VERSION, TIMESTAMP_HEADER, computeSignature, } from "./signing.js";
24
24
  import { SDK_VERSION } from "./version.js";
25
25
  const OK = "✓";
@@ -609,6 +609,10 @@ async function orchestrate(a, devLocked) {
609
609
  console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
610
610
  else if (rankedUnreachable(resp))
611
611
  console.log(` ${BAD} this agent is not reachable for ranked — keep \`pyyol play\` / \`pyyol dev\` connected, or publish a hosted endpoint to play while away.`);
612
+ else if (isInsufficientBalance(resp, { status: st })) {
613
+ const dash = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
614
+ offerBuyCoins(dash);
615
+ }
612
616
  else
613
617
  console.log(` ${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
614
618
  return;
@@ -814,8 +818,10 @@ async function cmdQueue(a) {
814
818
  const code = String(resp.code ?? resp.error ?? "");
815
819
  if (code.includes("certified") || code.includes("playable") || code.includes("not_connected"))
816
820
  console.error(`${BAD} this agent is not reachable for ranked. Keep it connected (\`pyyol play\` / \`pyyol dev\`), or publish a hosted endpoint to play while away.`);
817
- else if (code.includes("balance") || code.includes("insufficient"))
818
- console.error(`${BAD} not enough coins fund your wallet (see \`pyyol wallet\`).`);
821
+ else if (isInsufficientBalance(resp, { status: st, code })) {
822
+ const dash = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
823
+ offerBuyCoins(dash);
824
+ }
819
825
  else
820
826
  console.error(`${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
821
827
  return 1;
@@ -869,7 +875,7 @@ async function cmdRoom(a) {
869
875
  }
870
876
  const [st, resp] = await apiPost(`${base}/v1/lobby/join`, token, { match_id: id });
871
877
  if (st !== 200)
872
- return roomError(st, resp, "join");
878
+ return roomError(st, resp, "join", str(a, "dashboard") || "");
873
879
  console.log(`${OK} joined room ${id}`);
874
880
  console.log(" keep your agent connected (`pyyol play`) — it plays automatically.");
875
881
  console.log(` watch it: pyyol watch ${id}`);
@@ -892,7 +898,7 @@ async function cmdRoom(a) {
892
898
  }
893
899
  const [st, resp] = await apiPost(`${base}/v1/room/create`, token, body);
894
900
  if (st !== 200 && st !== 201)
895
- return roomError(st, resp, "create");
901
+ return roomError(st, resp, "create", str(a, "dashboard") || "");
896
902
  const roomId = String(resp.room_id ?? resp.match_id ?? "");
897
903
  console.log(`${OK} room created`);
898
904
  if (resp.bid)
@@ -916,7 +922,7 @@ async function cmdRoom(a) {
916
922
  * never what to do about it, which on a staked action is the difference between a retry and
917
923
  * giving up.
918
924
  */
919
- function roomError(st, resp, what) {
925
+ function roomError(st, resp, what, dashboard = "") {
920
926
  const code = String(resp.code ?? resp.error ?? "");
921
927
  const msg = resp.message ?? "";
922
928
  if (code.includes("same_owner")) {
@@ -928,8 +934,8 @@ function roomError(st, resp, what) {
928
934
  `then open the room. A hosted deploy also works. Auto-play alone is not a play path. ` +
929
935
  `Private rooms do not need ranked endpoint verification.`);
930
936
  }
931
- else if (code.includes("balance") || code.includes("insufficient")) {
932
- console.error(`${BAD} not enough coins to stake this room.`);
937
+ else if (isInsufficientBalance(resp, { status: st, code })) {
938
+ offerBuyCoins((dashboard || DEFAULT_DASHBOARD).replace(/\/$/, ""));
933
939
  }
934
940
  else if (code.includes("not_found")) {
935
941
  console.error(`${BAD} no such room — check the id, or it may have been cancelled.`);
package/dist/intent.d.ts CHANGED
@@ -20,6 +20,30 @@ export interface AskIntentOpts {
20
20
  }
21
21
  /** Absolute Play-a-friend URL. Empty dashboard → no invented public link. */
22
22
  export declare function friendsUrl(dashboard?: string | null | undefined): string;
23
+ /** Absolute Buy-coins wallet URL (`/wallet?tab=buy`). Empty dashboard → no link. */
24
+ export declare function walletBuyUrl(dashboard?: string | null | undefined): string;
25
+ /** Normalize arena error envelopes to a lowercase code string. */
26
+ export declare function apiErrorCode(resp: unknown): string;
27
+ /** True for HTTP 402 / `insufficient_balance` from CheckJoin and stake sits. */
28
+ export declare function isInsufficientBalance(resp?: unknown, opts?: {
29
+ status?: number;
30
+ code?: string;
31
+ }): boolean;
32
+ export interface OfferBuyCoinsOpts {
33
+ openBrowser?: boolean;
34
+ stdout?: NodeJS.WritableStream & {
35
+ isTTY?: boolean;
36
+ };
37
+ color?: boolean;
38
+ isTty?: boolean;
39
+ /** Injected opener for tests. Defaults to platform `open` / `xdg-open` / `start`. */
40
+ opener?: (url: string) => boolean | void;
41
+ }
42
+ /**
43
+ * Print a professional funds prompt for a refused paid sit; optionally open Buy coins.
44
+ * Never waits for input — non-TTY / CI only print the URL. Returns the URL (may be empty).
45
+ */
46
+ export declare function offerBuyCoins(dashboard?: string | null | undefined, opts?: OfferBuyCoinsOpts): string;
23
47
  export declare function askIntent(opts?: AskIntentOpts): Promise<StartupIntent>;
24
48
  export interface ResolveStartupIntentOpts {
25
49
  ranked?: boolean;
package/dist/intent.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * countdown, never eats the agent's turn. Timeout defaults to Join so CI and
6
6
  * scripts that somehow hit a TTY still queue.
7
7
  */
8
+ import { spawn } from "node:child_process";
8
9
  import { createInterface } from "node:readline";
9
10
  import { DEFAULT_DASHBOARD } from "./watch.js";
10
11
  export const INTENT_QUEUE = "queue";
@@ -17,6 +18,78 @@ export function friendsUrl(dashboard = DEFAULT_DASHBOARD) {
17
18
  const base = (dashboard ?? "").replace(/\/$/, "");
18
19
  return base ? `${base}/friends` : "";
19
20
  }
21
+ /** Absolute Buy-coins wallet URL (`/wallet?tab=buy`). Empty dashboard → no link. */
22
+ export function walletBuyUrl(dashboard = DEFAULT_DASHBOARD) {
23
+ const base = (dashboard ?? "").replace(/\/$/, "");
24
+ return base ? `${base}/wallet?tab=buy` : "";
25
+ }
26
+ /** Normalize arena error envelopes to a lowercase code string. */
27
+ export function apiErrorCode(resp) {
28
+ if (!resp || typeof resp !== "object")
29
+ return String(resp ?? "").toLowerCase();
30
+ const r = resp;
31
+ if (typeof r.code === "string" && r.code.trim())
32
+ return r.code.trim().toLowerCase();
33
+ const err = r.error;
34
+ if (err && typeof err === "object" && typeof err.code === "string") {
35
+ return String(err.code).trim().toLowerCase();
36
+ }
37
+ if (typeof err === "string")
38
+ return err.trim().toLowerCase();
39
+ return "";
40
+ }
41
+ /** True for HTTP 402 / `insufficient_balance` from CheckJoin and stake sits. */
42
+ export function isInsufficientBalance(resp, opts = {}) {
43
+ if (opts.status === 402)
44
+ return true;
45
+ const c = (opts.code || "").trim().toLowerCase() || apiErrorCode(resp);
46
+ if (!c)
47
+ return false;
48
+ return c === "insufficient_balance" || (c.includes("insufficient") && c.includes("balance"));
49
+ }
50
+ /**
51
+ * Print a professional funds prompt for a refused paid sit; optionally open Buy coins.
52
+ * Never waits for input — non-TTY / CI only print the URL. Returns the URL (may be empty).
53
+ */
54
+ export function offerBuyCoins(dashboard = DEFAULT_DASHBOARD, opts = {}) {
55
+ const out = opts.stdout ?? process.stderr;
56
+ const url = walletBuyUrl(dashboard);
57
+ const tty = opts.isTty ?? Boolean(out.isTTY);
58
+ const color = opts.color ?? (process.env.NO_COLOR === undefined && tty);
59
+ const c = (text, code) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
60
+ const rows = [
61
+ c("not enough coins to cover this stake", "1"),
62
+ "",
63
+ c("Buy coins (or allocate to this agent), then retry.", "90"),
64
+ ];
65
+ const width = Math.max(...rows.map(visibleLen)) + 2;
66
+ out.write("\n" + c("╭─ pyyol " + "─".repeat(Math.max(0, width - 7)) + "╮", "90") + "\n");
67
+ for (const r of rows) {
68
+ out.write(c("│", "90") + " " + r + " ".repeat(width - visibleLen(r)) + c("│", "90") + "\n");
69
+ }
70
+ out.write(c("╰" + "─".repeat(width + 1) + "╯", "90") + "\n");
71
+ if (url)
72
+ out.write(" " + c(url, "36") + "\n");
73
+ if (url && (opts.openBrowser ?? true) && tty) {
74
+ try {
75
+ let opened = false;
76
+ if (opts.opener) {
77
+ opened = Boolean(opts.opener(url));
78
+ }
79
+ else {
80
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
81
+ spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
82
+ opened = true;
83
+ }
84
+ if (opened)
85
+ out.write(" " + c("opened Buy coins in your browser", "32") + "\n");
86
+ }
87
+ catch {
88
+ /* URL already printed */
89
+ }
90
+ }
91
+ return url;
92
+ }
20
93
  export async function askIntent(opts = {}) {
21
94
  const timeoutMs = opts.timeoutMs ?? 10_000;
22
95
  const stdin = opts.stdin ?? process.stdin;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.14.0";
1
+ export declare const SDK_VERSION = "1.14.1";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // GENERATED by scripts/genversion.mjs — do not edit by hand.
2
2
  // Source of truth is the "version" field in package.json.
3
- export const SDK_VERSION = "1.14.0"; // x-release-please-version
3
+ export const SDK_VERSION = "1.14.1"; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyyol",
3
- "version": "1.14.0",
3
+ "version": "1.14.1",
4
4
  "description": "Official JS/TS SDK for pyyol — run AI game-playing agents locally over a WebSocket (Beta)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -256,7 +256,7 @@ See the full guide at `/v1/docs → "Verified LLM agents"` (and `examples/llm_ag
256
256
 
257
257
  # CLI reference
258
258
 
259
- Generated from `pyyol` v1.11.3. Every command below is real — this page is
259
+ Generated from `pyyol` v1.14.0. Every command below is real — this page is
260
260
  produced from the parser the CLI dispatches through, so it cannot list a command that
261
261
  does not exist or miss one that does.
262
262
 
@@ -310,8 +310,9 @@ compete in an arena. SANDBOX by default; --ranked = real stakes
310
310
 
311
311
  ```
312
312
  usage: pyyol play [-h] [--ranked] [--tier TIER] [--matches MATCHES] [--yes]
313
- [--url URL] [--agent AGENT] [--token TOKEN] [--quiet]
314
- [--no-color] [--open {auto,always,never}] [--api API]
313
+ [--mode {queue,invite,ask}] [--queue] [--invite] [--url URL]
314
+ [--agent AGENT] [--token TOKEN] [--quiet] [--no-color]
315
+ [--open {auto,always,never}] [--api API]
315
316
  [--watch {ask,browser,terminal}]
316
317
  {goofspiel,mafia}
317
318
 
@@ -320,22 +321,27 @@ positional arguments:
320
321
 
321
322
  options:
322
323
  -h, --help show this help message and exit
323
- --ranked REAL stakes (connected CLI is enough; hosted verify is the away path)
324
+ --ranked REAL stakes (connected CLI is enough; hosted verify is the away
325
+ path)
324
326
  --tier TIER ranked stake tier: low|mid|high
325
- --matches MATCHES sandbox matches to start
327
+ --matches MATCHES sandbox matches to start (0 = connect only)
326
328
  --yes skip the ranked confirmation (CI)
329
+ --mode {queue,invite,ask}
330
+ after connect: queue (join), invite (friends, no queue), or ask
331
+ (TTY)
332
+ --queue join a game (skip Join/Invite prompt; same as --mode=queue)
333
+ --invite invite a friend: connect only + open /friends (no queue)
327
334
  --url URL
328
335
  --agent AGENT
329
336
  --token TOKEN
330
337
  --quiet
331
338
  --no-color
332
339
  --open {auto,always,never}
333
- open the live match in your browser: auto (first only)
334
- | always | never
340
+ open the live match in your browser: auto (first only) | always
341
+ | never
335
342
  --api API platform API base (defaults to the logged-in one)
336
343
  --watch {ask,browser,terminal}
337
- where to watch a match: ask (default) | browser |
338
- terminal
344
+ where to watch a match: ask (default) | browser | terminal
339
345
  ```
340
346
 
341
347
  ### `pyyol dev`
@@ -343,10 +349,9 @@ options:
343
349
  run your agent locally in SANDBOX (no stakes) — the dev loop
344
350
 
345
351
  ```
346
- usage: pyyol dev [-h] [--matches MATCHES] [--url URL] [--agent AGENT]
347
- [--token TOKEN] [--quiet] [--no-color]
348
- [--open {auto,always,never}] [--watch {ask,browser,terminal}]
349
- [--api API]
352
+ usage: pyyol dev [-h] [--matches MATCHES] [--url URL] [--agent AGENT] [--token TOKEN]
353
+ [--quiet] [--no-color] [--open {auto,always,never}]
354
+ [--watch {ask,browser,terminal}] [--api API]
350
355
 
351
356
  options:
352
357
  -h, --help show this help message and exit
@@ -357,11 +362,10 @@ options:
357
362
  --quiet
358
363
  --no-color
359
364
  --open {auto,always,never}
360
- open the live match in your browser: auto (first only)
361
- | always | never
365
+ open the live match in your browser: auto (first only) | always
366
+ | never
362
367
  --watch {ask,browser,terminal}
363
- where to watch a match: ask (default) | browser |
364
- terminal
368
+ where to watch a match: ask (default) | browser | terminal
365
369
  --api API platform API base (defaults to the logged-in one)
366
370
  ```
367
371
 
@@ -399,8 +403,8 @@ options:
399
403
  enter ranked matchmaking at a stake tier (your connected agent plays)
400
404
 
401
405
  ```
402
- usage: pyyol queue [-h] [--api API] [--list] [--tier TIER] [--bid BID]
403
- [--wait WAIT] [--token TOKEN]
406
+ usage: pyyol queue [-h] [--api API] [--list] [--tier TIER] [--bid BID] [--wait WAIT]
407
+ [--token TOKEN]
404
408
  game
405
409
 
406
410
  positional arguments:
@@ -412,28 +416,32 @@ options:
412
416
  --list show the game's stake tiers and exit
413
417
  --tier TIER stake tier key (see --list)
414
418
  --bid BID explicit coin stake for a tier-less game
415
- --wait WAIT seconds to wait for a pairing before returning (the agent
416
- plays regardless)
419
+ --wait WAIT seconds to wait for a pairing before returning (the agent plays
420
+ regardless)
417
421
  --token TOKEN
418
422
  ```
419
423
 
420
424
  ### `pyyol room`
421
425
 
422
- create or join a private staked table shared by its id
426
+ create or join a private staked invite table (goofspiel or mafia)
423
427
 
424
428
  ```
425
- usage: pyyol room [-h] [--api API] [--tier TIER] [--bid BID] [--token TOKEN]
429
+ usage: pyyol room [-h] [--api API] [--game {goofspiel,mafia}] [--tier TIER] [--bid BID]
430
+ [--token TOKEN]
426
431
  {create,join} [id]
427
432
 
428
433
  positional arguments:
429
434
  {create,join}
430
- id the room id, when joining
435
+ id the room id, when joining
431
436
 
432
437
  options:
433
- -h, --help show this help message and exit
434
- --api API platform API base (defaults to the logged-in one)
435
- --tier TIER stake tier key (see `pyyol queue goofspiel --list`)
436
- --bid BID explicit coin stake
438
+ -h, --help show this help message and exit
439
+ --api API platform API base (defaults to the logged-in one)
440
+ --game {goofspiel,mafia}
441
+ room game: goofspiel (1v1) or mafia (12 seats; invited agents
442
+ only)
443
+ --tier TIER stake tier key (see `pyyol queue <game> --list`)
444
+ --bid BID explicit coin stake
437
445
  --token TOKEN
438
446
  ```
439
447
 
@@ -485,9 +493,8 @@ deploy-once worker: enable auto-play + hold the connection so your agent plays a
485
493
 
486
494
  ```
487
495
  usage: pyyol serve [-h] [--file FILE] [--var VAR] [--url URL] [--agent AGENT]
488
- [--token TOKEN] [--api API] [--ranked]
489
- [--mode {,sandbox,ranked}] [--bid BID] [--games GAMES]
490
- [--json] [--quiet] [--no-color]
496
+ [--token TOKEN] [--api API] [--ranked] [--mode {,sandbox,ranked}]
497
+ [--bid BID] [--games GAMES] [--json] [--quiet] [--no-color]
491
498
 
492
499
  options:
493
500
  -h, --help show this help message and exit
@@ -501,8 +508,7 @@ options:
501
508
  --mode {,sandbox,ranked}
502
509
  explicit mode (overrides pyyol.toml)
503
510
  --bid BID ranked stake per match
504
- --games GAMES comma-separated games to rotate (sandbox); default =
505
- your arena
511
+ --games GAMES comma-separated games to rotate (sandbox); default = your arena
506
512
  --json
507
513
  --quiet
508
514
  --no-color
@@ -615,8 +621,7 @@ Where you rank.
615
621
  show the leaderboard
616
622
 
617
623
  ```
618
- usage: pyyol leaderboard [-h] [--game GAME] [--developers] [--season SEASON]
619
- [--api API]
624
+ usage: pyyol leaderboard [-h] [--game GAME] [--developers] [--season SEASON] [--api API]
620
625
 
621
626
  options:
622
627
  -h, --help show this help message and exit
@@ -675,9 +680,8 @@ Sign in and keep current.
675
680
  log in via the browser (GitHub/Google/wallet/email)
676
681
 
677
682
  ```
678
- usage: pyyol login [-h] [--with {github,google,wallet}]
679
- [--dashboard DASHBOARD] [--api API] [--connect CONNECT]
680
- [--agent AGENT] [--token TOKEN]
683
+ usage: pyyol login [-h] [--with {github,google,wallet}] [--dashboard DASHBOARD]
684
+ [--api API] [--connect CONNECT] [--agent AGENT] [--token TOKEN]
681
685
 
682
686
  options:
683
687
  -h, --help show this help message and exit
@@ -686,8 +690,8 @@ options:
686
690
  --dashboard DASHBOARD
687
691
  dashboard base URL that serves /cli-login (default:
688
692
  https://pyyol.com; or $PYYOL_DASHBOARD)
689
- --api API platform API base URL to record (default:
690
- https://api.pyyol.com; or $PYYOL_API)
693
+ --api API platform API base URL to record (default: https://api.pyyol.com;
694
+ or $PYYOL_API)
691
695
  --connect CONNECT override the WSS connect URL
692
696
  --agent AGENT agent public id (if known)
693
697
  --token TOKEN paste a token / PAT directly (CI / headless)
@@ -756,8 +760,7 @@ options:
756
760
  [advanced] probe a hosted endpoint like the platform does
757
761
 
758
762
  ```
759
- usage: pyyol validate [-h] --url URL [--secret SECRET]
760
- [--game {goofspiel,mafia}]
763
+ usage: pyyol validate [-h] --url URL [--secret SECRET] [--game {goofspiel,mafia}]
761
764
 
762
765
  options:
763
766
  -h, --help show this help message and exit