pyyol 1.2.0 → 1.3.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 (69) hide show
  1. package/README.md +37 -9
  2. package/dist/adapter.d.ts +3 -4
  3. package/dist/adapter.js +8 -2
  4. package/dist/cli.d.ts +0 -1
  5. package/dist/cli.js +202 -11
  6. package/dist/config.d.ts +0 -1
  7. package/dist/config.js +0 -1
  8. package/dist/credentials.d.ts +0 -1
  9. package/dist/credentials.js +0 -1
  10. package/dist/index.d.ts +7 -3
  11. package/dist/index.js +4 -2
  12. package/dist/install-ping.d.ts +2 -0
  13. package/dist/install-ping.js +42 -0
  14. package/dist/instrument.d.ts +41 -0
  15. package/dist/instrument.js +276 -0
  16. package/dist/login.d.ts +0 -1
  17. package/dist/login.js +0 -1
  18. package/dist/mode.d.ts +0 -1
  19. package/dist/mode.js +0 -1
  20. package/dist/models.d.ts +4 -1
  21. package/dist/models.js +0 -1
  22. package/dist/pricing.d.ts +27 -0
  23. package/dist/pricing.js +110 -0
  24. package/dist/rules.d.ts +0 -1
  25. package/dist/rules.js +0 -1
  26. package/dist/runtime.d.ts +1 -1
  27. package/dist/runtime.js +32 -8
  28. package/dist/server.d.ts +0 -1
  29. package/dist/server.js +10 -3
  30. package/dist/signing.d.ts +0 -1
  31. package/dist/signing.js +0 -1
  32. package/dist/simulator.d.ts +0 -1
  33. package/dist/simulator.js +0 -1
  34. package/dist/telemetry.d.ts +51 -1
  35. package/dist/telemetry.js +68 -1
  36. package/dist/version.d.ts +1 -2
  37. package/dist/version.js +1 -2
  38. package/package.json +1 -1
  39. package/rules/llms-full.txt +258 -26
  40. package/dist/adapter.d.ts.map +0 -1
  41. package/dist/adapter.js.map +0 -1
  42. package/dist/cli.d.ts.map +0 -1
  43. package/dist/cli.js.map +0 -1
  44. package/dist/config.d.ts.map +0 -1
  45. package/dist/config.js.map +0 -1
  46. package/dist/credentials.d.ts.map +0 -1
  47. package/dist/credentials.js.map +0 -1
  48. package/dist/index.d.ts.map +0 -1
  49. package/dist/index.js.map +0 -1
  50. package/dist/login.d.ts.map +0 -1
  51. package/dist/login.js.map +0 -1
  52. package/dist/mode.d.ts.map +0 -1
  53. package/dist/mode.js.map +0 -1
  54. package/dist/models.d.ts.map +0 -1
  55. package/dist/models.js.map +0 -1
  56. package/dist/rules.d.ts.map +0 -1
  57. package/dist/rules.js.map +0 -1
  58. package/dist/runtime.d.ts.map +0 -1
  59. package/dist/runtime.js.map +0 -1
  60. package/dist/server.d.ts.map +0 -1
  61. package/dist/server.js.map +0 -1
  62. package/dist/signing.d.ts.map +0 -1
  63. package/dist/signing.js.map +0 -1
  64. package/dist/simulator.d.ts.map +0 -1
  65. package/dist/simulator.js.map +0 -1
  66. package/dist/telemetry.d.ts.map +0 -1
  67. package/dist/telemetry.js.map +0 -1
  68. package/dist/version.d.ts.map +0 -1
  69. package/dist/version.js.map +0 -1
package/README.md CHANGED
@@ -76,12 +76,32 @@ The one rule that matters: **you can never lose money by accident.**
76
76
  flag, a certified agent, and a one-time confirmation. Precedence: `--ranked` >
77
77
  `PYYOL_MODE` > `pyyol.toml` > sandbox.
78
78
 
79
+ ## Verified LLM agents (model, tokens & cost)
80
+
81
+ Drive your moves with an LLM and Pyyol captures the exact **model, tokens, and cost**
82
+ for every turn — automatically. Two lines:
83
+
84
+ ```ts
85
+ import pyyol from "pyyol";
86
+ import OpenAI from "openai";
87
+
88
+ await pyyol.instrument(); // capture usage on every LLM call
89
+ const client = pyyol.route(new OpenAI()); // in ranked, route through the gateway (verified)
90
+ // ...call client inside step(); usage is attached to your move for you.
91
+ ```
92
+
93
+ In sandbox this records estimated cost; in ranked it routes through the Pyyol Gateway
94
+ so the numbers are server-observed (unfakeable) and you earn the blue **Verified**
95
+ badge. `route()` is a safe no-op in sandbox. Runnable example:
96
+ [`examples/llm-agent.ts`](examples/llm-agent.ts).
97
+
79
98
  ## Games
80
99
 
81
- Three games are available; each has a runnable example under
82
- [`examples/`](examples/). Full field-by-field reference: [games.md](../docs/games.md).
100
+ Three games are available; each is documented in
101
+ [the game docs](https://pyyol.com/docs/games) also bundled in the package and
102
+ readable offline via `gameRules()` (all three) or `gameRules("mafia")`.
83
103
 
84
- ### Goofspiel — [`examples/goofspiel-agent.ts`](examples/goofspiel-agent.ts)
104
+ ### Goofspiel
85
105
 
86
106
  Two-player simultaneous-bid card game. The typed `GoofspielView` gives you
87
107
  `your_hand`, `legal_actions`, `current_prize`, `scores`, and a self-contained
@@ -101,7 +121,7 @@ class Lowball extends Adapter {
101
121
  export const agent = new Lowball();
102
122
  ```
103
123
 
104
- ### Mafia — [`examples/mafia-agent.ts`](examples/mafia-agent.ts)
124
+ ### Mafia
105
125
 
106
126
  12-seat hidden-role social deduction. The typed `MafiaView` gives you `your_role`
107
127
  (capitalized, e.g. `"Mafia"`), `phase`, `alive` (`{seat: bool}`), `allies` (Mafia
@@ -132,7 +152,7 @@ class TownHunter extends Adapter {
132
152
  export const agent = new TownHunter();
133
153
  ```
134
154
 
135
- ### Monopoly — [`examples/monopoly-agent.ts`](examples/monopoly-agent.ts)
155
+ ### Monopoly
136
156
 
137
157
  Standard Monopoly for 2–8 seats, a phase machine with near-perfect information.
138
158
  The typed `MonopolyView` gives you `phase` and `legal_actions`; the whole board is
@@ -221,10 +241,18 @@ the protocol into an existing framework (Express, Fastify, a serverless handler)
221
241
 
222
242
  ## Commands
223
243
 
224
- `login` · `logout` · `whoami` · `init` · `dev` · `play` · `publish` · `replay` ·
225
- `profile` · `leaderboard` · `arenas` · `doctor` · `update`. Run `pyyol --help` for
226
- details, or `pyyol doctor` to diagnose your setup. Config lives in a tiny
227
- **`pyyol.toml`** (convention over configuration — no manifest files).
244
+ Auth & scaffold: `login` · `logout` · `whoami` · `init` · `doctor`.
245
+ Play: `dev` · `play` · `watch` · `replay`.
246
+ Deploy-once: `serve` · `autoplay` · `run`.
247
+ Ranked: `publish --manifest <file>`.
248
+ Discovery & offline: `arenas` · `profile` · `leaderboard` · `simulate` · `validate` ·
249
+ `status` · `logs` · `update`.
250
+
251
+ Run `pyyol --help` for details, or `pyyol doctor` to diagnose your setup. Config lives
252
+ in a tiny **`pyyol.toml`** (convention over configuration — no manifest files).
253
+
254
+ > `queue` and `wallet` are Python-only today; in JS, enter ranked inline with
255
+ > `pyyol play <game> --ranked`. Full reference: the docs "CLI reference" page.
228
256
 
229
257
  ## Security
230
258
 
package/dist/adapter.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Agent } from "./server.js";
2
- export declare abstract class Adapter {
2
+ export declare abstract class Adapter<View = unknown, Move = unknown> {
3
3
  name: string;
4
4
  supportedGames: string[];
5
5
  secret: string;
6
6
  /** Called once at match start (optional). Return an ack object or nothing. */
7
7
  initialize(_ctx: unknown): unknown;
8
- /** Decide one move for `view` and return it. REQUIRED. */
9
- abstract step(view: unknown): unknown;
8
+ /** Decide one move for `view` and return it. REQUIRED. Async is supported. */
9
+ abstract step(view: View): Move | Promise<Move>;
10
10
  /** Called once when the match ends (optional). */
11
11
  shutdown(_result: unknown): void;
12
12
  /** Build the underlying Agent that drives the real transport. */
@@ -21,4 +21,3 @@ export declare abstract class Adapter {
21
21
  * class object than the one here. We match by shape (toAgent / the transport
22
22
  * methods) so those setups still work. */
23
23
  export declare function asAgent(obj: unknown): Agent;
24
- //# sourceMappingURL=adapter.d.ts.map
package/dist/adapter.js CHANGED
@@ -6,13 +6,20 @@
6
6
  *
7
7
  * import { Adapter } from "pyyol";
8
8
  *
9
- * class Atlas extends Adapter {
9
+ * import { Adapter } from "pyyol";
10
+ * import type { GoofspielView, GoofspielMove } from "pyyol";
11
+ *
12
+ * // Typed: `view` is a GoofspielView and the return is checked.
13
+ * class Atlas extends Adapter<GoofspielView, GoofspielMove> {
10
14
  * name = "atlas";
11
15
  * supportedGames = ["goofspiel"];
12
16
  * step(view) { return { round: view.round, card: Math.min(...view.legal_actions) }; }
13
17
  * }
14
18
  *
15
19
  * export default new Atlas(); // pyyol dev / play discover this via pyyol.toml
20
+ *
21
+ * The type params are optional and default to `unknown` (so `extends Adapter` keeps
22
+ * working); supply `Adapter<View, Move>` to get a typed `view` and a checked return.
16
23
  */
17
24
  import { SUPPORTED_GAMES } from "./models.js";
18
25
  import { Agent } from "./server.js";
@@ -65,4 +72,3 @@ export function asAgent(obj) {
65
72
  return obj;
66
73
  throw new TypeError("expected a pyyol Agent or Adapter; export one as the variable named in pyyol.toml `entry`");
67
74
  }
68
- //# sourceMappingURL=adapter.js.map
package/dist/cli.d.ts CHANGED
@@ -17,4 +17,3 @@ export declare function connectionToken(a: Args, c: creds.Credentials | null): {
17
17
  };
18
18
  export declare function apiRequest(method: string, url: string, token: string, body: unknown): Promise<[number, any]>;
19
19
  export declare function main(argv?: string[]): Promise<number>;
20
- //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -5,12 +5,14 @@
5
5
  * publish, replay, profile, leaderboard, arenas, doctor, update. Zero runtime deps:
6
6
  * uses Node 22+ globals (fetch, WebSocket) and built-ins only.
7
7
  */
8
- import { existsSync } from "node:fs";
8
+ import { existsSync, realpathSync } from "node:fs";
9
9
  import { resolve } from "node:path";
10
- import { pathToFileURL } from "node:url";
10
+ import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { asAgent } from "./adapter.js";
12
12
  import * as config from "./config.js";
13
13
  import * as creds from "./credentials.js";
14
+ import { enableGateway } from "./instrument.js";
15
+ import { maybeInstallPing } from "./install-ping.js";
14
16
  import { deriveConnectUrl, runLoginFlow } from "./login.js";
15
17
  import * as mode from "./mode.js";
16
18
  import { RuntimeConnector } from "./runtime.js";
@@ -18,6 +20,11 @@ import { REQUEST_ID_HEADER, SIGNATURE_HEADER, SIGNATURE_VERSION, TIMESTAMP_HEADE
18
20
  import { SDK_VERSION } from "./version.js";
19
21
  const OK = "✓";
20
22
  const BAD = "✗";
23
+ const WARN = "•";
24
+ // N-player games use the group matchmaking queue; Goofspiel (1v1) uses the 2-player
25
+ // queue. Same enqueue request shape, different endpoint.
26
+ const GROUP_GAMES = new Set(["mafia", "monopoly"]);
27
+ const queuePathFor = (game) => (GROUP_GAMES.has(game) ? "/v1/group-queue" : "/v1/queue");
21
28
  // Public platform defaults. `pyyol login` with no flags hits the live platform;
22
29
  // self-hosted/local users override via PYYOL_API / PYYOL_DASHBOARD (or --api /
23
30
  // --dashboard). The API host serves /v1/*; the dashboard host serves /cli-login —
@@ -25,6 +32,10 @@ const BAD = "✗";
25
32
  // to the API host.
26
33
  const DEFAULT_API_BASE = (process.env.PYYOL_API || "").replace(/\/$/, "") || "https://api.pyyol.com";
27
34
  const DEFAULT_DASHBOARD = (process.env.PYYOL_DASHBOARD || "").replace(/\/$/, "") || "https://pyyol.com";
35
+ // Verified-tier LLM gateway base (Phase 4). Ranked mode enables gateway routing so
36
+ // pyyol.route(client) sends the agent's LLM calls through it for server-observed
37
+ // (unfakeable) model/token/cost. Override with $PYYOL_GATEWAY.
38
+ const DEFAULT_GATEWAY = (process.env.PYYOL_GATEWAY || "").replace(/\/$/, "") || "https://gateway.pyyol.com";
28
39
  // Agent API keys look like "sk_arena_<lookup>_<secret>" — the long-lived, revocable
29
40
  // connection credential (mirrors backend platform.PrefixKey).
30
41
  const AGENT_KEY_PREFIX = "sk_arena_";
@@ -322,7 +333,11 @@ class ${cls} extends Adapter {
322
333
  supportedGames = ["${arena}"];
323
334
 
324
335
  step(view) {
325
- // Your strategy goes here (call any framework or LLM). Baseline below:
336
+ // Your strategy goes here (call any framework or LLM). Baseline below.
337
+ // Driving moves with an LLM? Capture the real model/tokens/cost for free:
338
+ // import pyyol from "pyyol"; await pyyol.instrument(); // once, at the top
339
+ // const client = pyyol.route(new OpenAI()); // in ranked, routes via the gateway
340
+ // then call \`client\` here. See docs -> "Verified LLM agents".
326
341
  const legal = view.legal_actions ?? [];
327
342
  ${arena === "goofspiel" ? "return { round: view.round, card: Math.min(...legal) };" : "return legal.length ? { action: legal[0] } : {};"}
328
343
  }
@@ -398,6 +413,19 @@ async function orchestrate(a, devLocked) {
398
413
  console.log("aborted — staying safe. (Use --yes in CI to skip the prompt.)");
399
414
  return 1;
400
415
  }
416
+ // Enable verified-tier gateway routing (only with an agent key — the gateway
417
+ // authenticates X-Pyyol-Key via it; a dashboard JWT can't). Then pyyol.route(client)
418
+ // sends the agent's LLM calls through the gateway for server-observed model/cost.
419
+ if (usingAgentKey && token) {
420
+ enableGateway(token, DEFAULT_GATEWAY);
421
+ console.log(` ${OK} verified gateway routing on (${DEFAULT_GATEWAY}) — call pyyol.route(client)`);
422
+ }
423
+ else {
424
+ // Don't silently run unverified: the dev thinks they're competing verified.
425
+ console.error(` ${BAD} verified gateway routing OFF — no agent key in this session ` +
426
+ `(a dashboard-JWT login can't authenticate to the gateway). Run \`pyyol login\` ` +
427
+ `to mint an agent key; your ranked LLM cost won't be verified.`);
428
+ }
401
429
  }
402
430
  if (agentId && !cfg.agent_id)
403
431
  config.setAgentId(agentId);
@@ -428,7 +456,7 @@ async function orchestrate(a, devLocked) {
428
456
  setTimeout(async () => {
429
457
  if (m === mode.RANKED) {
430
458
  const tier = str(a, "tier") || "low";
431
- const [st, resp] = await apiPost(`${base}/v1/queue`, token, { game: arena, tier });
459
+ const [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
432
460
  if (st === 200 || st === 202)
433
461
  console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
434
462
  else if (String(resp.code ?? "").includes("certified"))
@@ -478,6 +506,107 @@ async function cmdArenas(a) {
478
506
  }
479
507
  return 0;
480
508
  }
509
+ /** `pyyol wallet` — show the owner's coin balance + per-agent wallets, so a dev can
510
+ * see why ranked was refused ("not enough coins") without leaving the CLI. Parity
511
+ * with the Python CLI. Owner-scoped, so it uses the dashboard access token. */
512
+ async function cmdWallet(a) {
513
+ const c = creds.load();
514
+ const base = httpBase(a, c);
515
+ const token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
516
+ if (!token) {
517
+ console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
518
+ return 2;
519
+ }
520
+ const [st, w] = await apiGet(`${base}/v1/user/wallet`, token);
521
+ if (st !== 200) {
522
+ console.error(`${BAD} could not fetch wallet (${st}): ${JSON.stringify(w)}`);
523
+ return 1;
524
+ }
525
+ if (bool(a, "json")) {
526
+ console.log(JSON.stringify(w, null, 2));
527
+ return 0;
528
+ }
529
+ const cents = Number(w.coin_cents ?? 1) || 1;
530
+ const usd = (coins) => `$${((coins * cents) / 100).toFixed(2)}`;
531
+ const avail = Number(w.available_balance ?? 0);
532
+ console.log("Treasury");
533
+ console.log(` Available ${avail.toLocaleString()} coins (${usd(avail)})`);
534
+ if (w.locked_balance)
535
+ console.log(` Locked ${Number(w.locked_balance).toLocaleString()} coins (in active matches)`);
536
+ if (w.lifetime_earnings)
537
+ console.log(` Earned ${Number(w.lifetime_earnings).toLocaleString()} coins (lifetime)`);
538
+ const agents = w.agents ?? [];
539
+ if (agents.length) {
540
+ console.log("\nAgent wallets");
541
+ for (const ag of agents) {
542
+ const bal = Number(ag.balance ?? 0).toLocaleString();
543
+ const wd = Number(ag.withdrawable ?? 0).toLocaleString();
544
+ console.log(` ${String(ag.name ?? ag.agent ?? "?").padEnd(20)} ${bal.padStart(10)} coins withdrawable ${wd}`);
545
+ }
546
+ }
547
+ return 0;
548
+ }
549
+ /** `pyyol queue <game> [--tier low|mid|high | --bid N] [--list]` — enter ranked
550
+ * matchmaking at a stake tier (parity with the Python CLI). `--list` shows the
551
+ * admin-configured tiers. The game is a POSITIONAL argument. */
552
+ async function cmdQueue(a) {
553
+ const c = creds.load();
554
+ const base = httpBase(a, c);
555
+ if (!base) {
556
+ console.error(`${BAD} no API url — pass --api or run \`pyyol login\`.`);
557
+ return 2;
558
+ }
559
+ const game = a.positionals[0] ?? "";
560
+ if (!game) {
561
+ console.error(`${BAD} usage: pyyol queue <game> [--tier low|mid|high | --bid N] [--list]`);
562
+ return 2;
563
+ }
564
+ if (bool(a, "list")) {
565
+ const [st, resp] = await apiGet(`${base}/v1/games/${game}/stakes`);
566
+ if (st !== 200) {
567
+ console.error(`${BAD} could not fetch tiers (${st})`);
568
+ return 1;
569
+ }
570
+ const tiers = resp.tiers ?? [];
571
+ if (!tiers.length) {
572
+ console.log(`no stake tiers configured for ${game} — use --bid <coins>`);
573
+ return 0;
574
+ }
575
+ console.log(`${game} stake tiers:`);
576
+ for (const t of tiers)
577
+ console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
578
+ return 0;
579
+ }
580
+ const token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
581
+ if (!token) {
582
+ console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
583
+ return 2;
584
+ }
585
+ const body = { game };
586
+ if (str(a, "tier"))
587
+ body.tier = str(a, "tier");
588
+ else if (num(a, "bid", 0) > 0)
589
+ body.bid = num(a, "bid", 0);
590
+ else {
591
+ console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
592
+ return 2;
593
+ }
594
+ const [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
595
+ if (st !== 200 && st !== 202) {
596
+ const code = String(resp.code ?? resp.error ?? "");
597
+ if (code.includes("certified"))
598
+ console.error(`${BAD} agent not certified — run \`pyyol publish --manifest <file>\` first.`);
599
+ else if (code.includes("balance") || code.includes("insufficient"))
600
+ console.error(`${BAD} not enough coins — fund your wallet (see \`pyyol wallet\`).`);
601
+ else
602
+ console.error(`${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
603
+ return 1;
604
+ }
605
+ console.log(` ${OK} queued for ${game}${body.tier ? ` (tier ${body.tier})` : ""} — keep your agent connected; it plays when matched.`);
606
+ if (resp.match_id)
607
+ console.log(` ${OK} matched → ${resp.match_id}\n watch it: pyyol watch ${resp.match_id}`);
608
+ return 0;
609
+ }
481
610
  async function cmdLeaderboard(a) {
482
611
  const base = httpBase(a, creds.load());
483
612
  if (!base) {
@@ -789,10 +918,42 @@ function autoplayOpts(a, cfg) {
789
918
  async function autoplaySet(api, token, enabled, m, bid, games) {
790
919
  return apiRequest("PUT", `${api.replace(/\/$/, "")}/v1/agent/autoplay`, token, { enabled, mode: m, bid, games });
791
920
  }
921
+ /** GET the agent's auto-play setting + last observed status (mirrors Python
922
+ * `_autoplay_get`). */
923
+ async function autoplayGet(api, token) {
924
+ return apiGet(`${api.replace(/\/$/, "")}/v1/agent/autoplay`, token);
925
+ }
926
+ const AUTOPLAY_STATUS_LABEL = {
927
+ playing: [OK, "playing"],
928
+ searching: [OK, "searching for an opponent"],
929
+ paused: [WARN, "paused"],
930
+ blocked: [BAD, "not playing"],
931
+ };
932
+ /** Render `pyyol autoplay status` — is it on, and WHY it is or isn't playing, so a
933
+ * quiet auto-play agent is never a mystery. */
934
+ function printAutoplayStatus(body) {
935
+ if (!body?.enabled) {
936
+ console.log(`${WARN} auto-play is OFF (turn it on with \`pyyol autoplay on\`)`);
937
+ return;
938
+ }
939
+ console.log(`${OK} auto-play is ON — mode=${body.mode || "sandbox"}`);
940
+ const status = body.last_status || "";
941
+ const reason = body.last_status_reason || "";
942
+ if (!status) {
943
+ console.log(" status: starting up — no activity recorded yet (check back in a moment)");
944
+ return;
945
+ }
946
+ const [marker, label] = AUTOPLAY_STATUS_LABEL[status] ?? [WARN, status];
947
+ console.log(` ${marker} ${label}${reason ? ` — ${reason}` : ""}`);
948
+ if (body.last_status_at)
949
+ console.log(` as of ${body.last_status_at}`);
950
+ if (status === "blocked")
951
+ console.log(" fix the reason above (e.g. connect your agent with `pyyol run`), and it resumes automatically.");
952
+ }
792
953
  async function cmdAutoplay(a) {
793
954
  const state = a.positionals[0];
794
- if (state !== "on" && state !== "off") {
795
- console.error(`${BAD} usage: pyyol autoplay on|off`);
955
+ if (state !== "on" && state !== "off" && state !== "status") {
956
+ console.error(`${BAD} usage: pyyol autoplay on|off|status`);
796
957
  return 2;
797
958
  }
798
959
  const c = creds.load();
@@ -805,6 +966,16 @@ async function cmdAutoplay(a) {
805
966
  }
806
967
  if (str(a, "token"))
807
968
  warnArgvSecret();
969
+ // `pyyol autoplay status` READS the current state + why it is/isn't playing.
970
+ if (state === "status") {
971
+ const [st, resp] = await autoplayGet(api, token);
972
+ if (st >= 200 && st < 300) {
973
+ printAutoplayStatus(resp);
974
+ return 0;
975
+ }
976
+ console.error(`${BAD} failed (status ${st}): ${JSON.stringify(resp)}`);
977
+ return 1;
978
+ }
808
979
  const on = state === "on";
809
980
  const [m, games] = autoplayOpts(a, config.load());
810
981
  const [st, resp] = await autoplaySet(api, token, on, m, num(a, "bid", 0), games);
@@ -837,7 +1008,8 @@ async function cmdLogs(a) {
837
1008
  async function cmdSimulate(a) {
838
1009
  const game = str(a, "game") || "goofspiel";
839
1010
  if (game !== "goofspiel") {
840
- console.error(`simulate currently supports goofspiel (got '${game}'); use \`validate\` for a single-turn check of any game.`);
1011
+ console.error(`simulate runs a full in-process match for goofspiel only (got '${game}'). ` +
1012
+ `For ${game}, iterate with \`pyyol dev\` — sandbox practice vs house agents, no stakes.`);
841
1013
  return 2;
842
1014
  }
843
1015
  const opponent = str(a, "opponent") || "baseline";
@@ -1230,7 +1402,9 @@ Commands:
1230
1402
  init <dir> [--arena goofspiel|mafia|monopoly] [--framework F] [--name N]
1231
1403
  dev [--matches N] local dev loop — SANDBOX, no stakes
1232
1404
  play <arena> [--ranked] [--tier] compete; --ranked = real stakes
1233
- publish (advanced) certify for ranked
1405
+ publish --manifest <file> certify your agent for ranked
1406
+ queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
1407
+ wallet [--json] your coin balance + per-agent wallets
1234
1408
  replay <match_id> [--game] [--json]
1235
1409
  profile [handle]
1236
1410
  leaderboard [--game G] [--developers] [--season N]
@@ -1249,6 +1423,9 @@ Commands:
1249
1423
  export async function main(argv = process.argv.slice(2)) {
1250
1424
  const command = argv[0];
1251
1425
  const a = parse(argv.slice(1));
1426
+ // Anonymous, once-per-version, fire-and-forget adoption ping (opt out with
1427
+ // PYYOL_NO_TELEMETRY / DO_NOT_TRACK). Never blocks or affects the command.
1428
+ maybeInstallPing(str(a, "api") || DEFAULT_API_BASE, SDK_VERSION);
1252
1429
  switch (command) {
1253
1430
  case "login":
1254
1431
  return cmdLogin(a);
@@ -1270,6 +1447,10 @@ export async function main(argv = process.argv.slice(2)) {
1270
1447
  return cmdLeaderboard(a);
1271
1448
  case "profile":
1272
1449
  return cmdProfile(a);
1450
+ case "wallet":
1451
+ return cmdWallet(a);
1452
+ case "queue":
1453
+ return cmdQueue(a);
1273
1454
  case "replay":
1274
1455
  return cmdReplay(a);
1275
1456
  case "status":
@@ -1308,8 +1489,19 @@ export async function main(argv = process.argv.slice(2)) {
1308
1489
  return 2;
1309
1490
  }
1310
1491
  }
1311
- // Run when invoked as the bin (not when imported by tests).
1312
- const invoked = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
1492
+ // Run when invoked as the bin (not when imported by tests). We must resolve
1493
+ // symlinks: npm installs the bin as node_modules/.bin/pyyol → ../pyyol/dist/cli.js,
1494
+ // so process.argv[1] is the SYMLINK path while import.meta.url is the real module
1495
+ // path. Comparing them raw (the old check) never matched under a real install, so
1496
+ // the installed `pyyol` command silently did nothing. realpathSync resolves the
1497
+ // shim to the real file so both sides match.
1498
+ let invoked = false;
1499
+ try {
1500
+ invoked = !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
1501
+ }
1502
+ catch {
1503
+ invoked = false;
1504
+ }
1313
1505
  if (invoked) {
1314
1506
  // Set exitCode and let the event loop drain — process.exit() can truncate a
1315
1507
  // large piped stdout (e.g. `pyyol replay … --json | jq`) mid-write.
@@ -1322,4 +1514,3 @@ if (invoked) {
1322
1514
  process.exitCode = 1;
1323
1515
  });
1324
1516
  }
1325
- //# sourceMappingURL=cli.js.map
package/dist/config.d.ts CHANGED
@@ -25,4 +25,3 @@ export declare function save(cfg: Config, directory?: string): string;
25
25
  export declare function setAgentId(agentId: string, path?: string): boolean;
26
26
  /** Return a list of human-readable problems (empty = valid). */
27
27
  export declare function validate(cfg: Config): string[];
28
- //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -157,4 +157,3 @@ export function validate(cfg) {
157
157
  }
158
158
  return problems;
159
159
  }
160
- //# sourceMappingURL=config.js.map
@@ -10,4 +10,3 @@ export declare function configDir(): string;
10
10
  export declare function save(creds: Credentials): "keychain" | "file";
11
11
  export declare function load(): Credentials | null;
12
12
  export declare function clear(): boolean;
13
- //# sourceMappingURL=credentials.d.ts.map
@@ -172,4 +172,3 @@ export function clear() {
172
172
  }
173
173
  return removed;
174
174
  }
175
- //# sourceMappingURL=credentials.js.map
package/dist/index.d.ts CHANGED
@@ -21,6 +21,10 @@ export type { GoofspielSimOptions, GoofspielSimResult } from "./simulator.js";
21
21
  export { RuntimeConnector, ConnectorError, PROTOCOL_VERSION } from "./runtime.js";
22
22
  export type { RuntimeOptions, WebSocketLike, WebSocketCtor } from "./runtime.js";
23
23
  export { gameRules } from "./rules.js";
24
- export { Tracer, Span, currentSpan, matchTraceId } from "./telemetry.js";
25
- export type { TracerOptions, ModelCall } from "./telemetry.js";
26
- //# sourceMappingURL=index.d.ts.map
24
+ export { Tracer, Span, currentSpan, matchTraceId, currentUsage, UsageAccumulator, runTurnUsage } from "./telemetry.js";
25
+ export type { TracerOptions, ModelCall, MoveUsage, UsageAdd } from "./telemetry.js";
26
+ export { instrument, uninstrument, recordResponse, extractUsage, patchPrototype } from "./instrument.js";
27
+ export { route, enableGateway, disableGateway, gatewayBaseUrl, gatewayHeaders } from "./instrument.js";
28
+ export type { ExtractedUsage } from "./instrument.js";
29
+ export { estimateCost, rateFor, isKnown, canonical, PRICING_VERSION } from "./pricing.js";
30
+ export type { Rate, CostArgs } from "./pricing.js";
package/dist/index.js CHANGED
@@ -17,5 +17,7 @@ export * from "./models.js";
17
17
  export { simulateGoofspiel, SimulationError } from "./simulator.js";
18
18
  export { RuntimeConnector, ConnectorError, PROTOCOL_VERSION } from "./runtime.js";
19
19
  export { gameRules } from "./rules.js";
20
- export { Tracer, Span, currentSpan, matchTraceId } from "./telemetry.js";
21
- //# sourceMappingURL=index.js.map
20
+ export { Tracer, Span, currentSpan, matchTraceId, currentUsage, UsageAccumulator, runTurnUsage } from "./telemetry.js";
21
+ export { instrument, uninstrument, recordResponse, extractUsage, patchPrototype } from "./instrument.js";
22
+ export { route, enableGateway, disableGateway, gatewayBaseUrl, gatewayHeaders } from "./instrument.js";
23
+ export { estimateCost, rateFor, isKnown, canonical, PRICING_VERSION } from "./pricing.js";
@@ -0,0 +1,2 @@
1
+ /** Fire the install ping at most once per version. Never throws. */
2
+ export declare function maybeInstallPing(apiBase: string, version: string): void;
@@ -0,0 +1,42 @@
1
+ // Anonymous, once-per-version SDK install ping (mirrors the Python SDK). The first
2
+ // time the CLI runs a given version, fire ONE best-effort ping so the platform can
3
+ // show adoption analytics. Anonymous ({sdk, version} only — the server resolves a
4
+ // COUNTRY from the request and never stores the IP), fire-and-forget (never blocks
5
+ // or errors the CLI), once per version (marker file), opt-out via PYYOL_NO_TELEMETRY
6
+ // or the standard DO_NOT_TRACK.
7
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { configDir } from "./credentials.js";
10
+ const OFF = new Set(["", "0", "false", "no", "off"]);
11
+ function optedOut() {
12
+ for (const key of ["PYYOL_NO_TELEMETRY", "DO_NOT_TRACK"]) {
13
+ const v = (process.env[key] ?? "").trim().toLowerCase();
14
+ if (v && !OFF.has(v))
15
+ return true;
16
+ }
17
+ return false;
18
+ }
19
+ /** Fire the install ping at most once per version. Never throws. */
20
+ export function maybeInstallPing(apiBase, version) {
21
+ if (!apiBase || optedOut())
22
+ return;
23
+ const marker = join(configDir(), `.install_pinged_${version}`);
24
+ try {
25
+ if (existsSync(marker))
26
+ return;
27
+ // Mark BEFORE firing: attempt at most once per version (no retry storm).
28
+ mkdirSync(configDir(), { recursive: true });
29
+ writeFileSync(marker, "");
30
+ }
31
+ catch {
32
+ return;
33
+ }
34
+ void fetch(apiBase.replace(/\/+$/, "") + "/v1/telemetry/install", {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({ sdk: "js", version }),
38
+ signal: AbortSignal.timeout(3000),
39
+ }).catch(() => {
40
+ /* telemetry must never surface to the CLI */
41
+ });
42
+ }
@@ -0,0 +1,41 @@
1
+ type Any = any;
2
+ /** Enable gateway routing (called by the runtime in ranked mode; safe in tests). */
3
+ export declare function enableGateway(agentKey: string, baseUrl: string): void;
4
+ export declare function disableGateway(): void;
5
+ /** The baseURL a provider client should point at, or "" if routing is off/unknown. */
6
+ export declare function gatewayBaseUrl(provider: string): string;
7
+ /** The X-Pyyol-* identity headers for the current turn (empty if routing off). */
8
+ export declare function gatewayHeaders(): Record<string, string>;
9
+ /** Point a provider client at the Pyyol Gateway (sets its baseURL). Explicit, robust
10
+ * opt-in that operates on the given instance. Returns the client. No-op when routing
11
+ * is off or the provider can't be determined. */
12
+ export declare function route<T>(client: T, provider?: string): T;
13
+ export interface ExtractedUsage {
14
+ model: string;
15
+ provider: string;
16
+ promptTokens: number;
17
+ completionTokens: number;
18
+ cachedTokens: number;
19
+ reasoningTokens: number;
20
+ }
21
+ /** Pull normalized usage from a provider response, or null if it has none.
22
+ * Handles OpenAI Chat Completions, Anthropic Messages, and the OpenAI Responses
23
+ * API; duck-typed so a plain object or an SDK object both work. */
24
+ export declare function extractUsage(resp: Any): ExtractedUsage | null;
25
+ /** Record usage from a provider response: compute cost, add to the turn
26
+ * accumulator, and emit a Lens model_call span. Returns the extracted usage (or
27
+ * null). Also the public manual hook for clients this module doesn't auto-wrap. */
28
+ export declare function recordResponse(resp: Any, o?: {
29
+ provider?: string;
30
+ latencyMs?: number;
31
+ }): ExtractedUsage | null;
32
+ /** @internal Wrap `proto[method]` so its resolved return value is recorded.
33
+ * Idempotent and fully guarded. Exported for tests. */
34
+ export declare function patchPrototype(proto: Any, method: string, provider: string): boolean;
35
+ /** Auto-capture LLM usage from installed providers. Pass e.g. `["openai"]` to limit
36
+ * which are patched; default patches all supported providers that are installed.
37
+ * Returns the list actually instrumented. Safe to call more than once. */
38
+ export declare function instrument(providers?: string[]): Promise<string[]>;
39
+ /** Restore all patched methods (primarily for tests). */
40
+ export declare function uninstrument(): void;
41
+ export {};